<?php
/**
 * Utils.php
 *
 * @since      1.0.0
 * @author     alfiopiccione <alfio.piccione@gmail.com>
 * @license    http://opensource.org/licenses/gpl-2.0.php GPL v2
 *
 * Copyright (C) 2025 POP
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

namespace WcElectronInvoice\Functions;

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

use WcElectronInvoice\Admin\Settings\OptionPage;
use WcElectronInvoice\Integrations;
use WcElectronInvoice\Plugin;
use WcElectronInvoice\Providers\OrderQuery;
use WcElectronInvoice\Sanitize\Arrays;
use WcElectronInvoice\Sanitize\Text;
use WcElectronInvoice\WooCommerce\Fields\GeneralFields;
use WcElectronInvoice\WooCommerce\Fields\InvoiceFields;
use WcElectronInvoice\Xml\CreateXml;

/**
 * Filter Input
 *
 * @param array  $data    The haystack of the elements.
 * @param string $key     The key of the element within the haystack to filter.
 * @param int    $filter  The filter to use.
 * @param array  $options The option for the filter var.
 *
 * @return bool|mixed The value filtered on success false if filter fails or key doesn't exists.
 * @uses  filter_var() To filter the value.
 *
 * @since 1.0.0
 *
 */
function filterInput($data, $key, $filter = FILTER_DEFAULT, $options = array())
{
    // Support for PHP >= 8.1
    // 513 = FILTER_SANITIZE_STRING
    if ((phpversion() >= 8.1) && $filter === 513) {
        $filter = FILTER_UNSAFE_RAW;
    }

    if (isset($data[$key])) {
        // If the value is an array, filter recursively
        if (is_array($data[$key])) {
            return array_map(function ($value) use ($filter, $options) {
                return filter_var($value, $filter, $options);
            }, $data[$key]);
        }

        // Filter single value
        return filter_var($data[$key], $filter, $options);
    }

    return false;
}

/**
 * Resolve the current user plan level from the wizard cache when available.
 *
 * @return string
 */
function getCurrentUserPlanLevel(): string
{
    $cachedLevel = (string)get_option('pop_user_level', '');

    if (class_exists('\\WcElectronInvoice\\Wizard') &&
        method_exists('\\WcElectronInvoice\\Wizard', 'popGetUserLevel')
    ) {
        $resolvedLevel = (string)\WcElectronInvoice\Wizard::popGetUserLevel();
        if ('' !== $resolvedLevel) {
            return $resolvedLevel;
        }
    }

    return '' !== $cachedLevel ? $cachedLevel : 'free';
}

/**
 * Compare plan levels using the public POP level ordering.
 *
 * @param string $level
 * @param string $compare
 *
 * @return bool
 */
function isUserPlanLevelGte(string $level, string $compare): bool
{
    $levels = ['free', 'basic', 'growth', 'pro', 'enterprise'];

    $levelIndex   = array_search($level, $levels, true);
    $compareIndex = array_search($compare, $levels, true);

    if (false === $levelIndex || false === $compareIndex) {
        return false;
    }

    return $levelIndex >= $compareIndex;
}

/**
 * Return the premium badge label based on the required plan and current level.
 *
 * Free users still see the generic paid label, while paid users see the exact
 * entry plan for the locked feature.
 *
 * @param string      $requiredLevel
 * @param string|null $currentLevel
 *
 * @return string
 */
function getPremiumBadgeLabel(string $requiredLevel, ?string $currentLevel = null): string
{
    $currentLevel = null !== $currentLevel && '' !== $currentLevel ? $currentLevel : getCurrentUserPlanLevel();

    if (! isUserPlanLevelGte($currentLevel, 'basic')) {
        return esc_html__('Paid only', WC_EL_INV_TEXTDOMAIN);
    }

    return sprintf(
        esc_html__('From %s', WC_EL_INV_TEXTDOMAIN),
        ucfirst($requiredLevel)
    );
}

/**
 * Return whether the current plan can create and manage credit notes.
 *
 * @param string|null $level Optional explicit level override.
 *
 * @return bool
 */
function isCreditNotePlanEnabled(?string $level = null): bool
{
    $level = null !== $level && '' !== $level ? $level : getCurrentUserPlanLevel();

    return isUserPlanLevelGte($level, 'basic');
}

/**
 * Convert HTML and shortcode content to readable plain text.
 *
 * @param mixed $string
 * @param bool  $removeBreaks
 *
 * @return string
 */
function plainTextFromHtml($string, $removeBreaks = false): string
{
    if (null === $string) {
        return '';
    }

    if (! is_scalar($string)) {
        return '';
    }

    $string = (string)$string;
    if ('' === $string) {
        return '';
    }

    $string = preg_replace('@<(script|style)[^>]*?>.*?</\\1>@si', ' ', $string);
    $string = preg_replace('/<img[^>]+\>/i', ' ', $string);
    $string = preg_replace('/<(br|\/p|\/div|\/li|\/tr|\/td|\/th|\/h[1-6])[^>]*>/i', "\n", $string);
    $string = strip_shortcodes($string);
    $string = strip_tags($string);
    $string = html_entity_decode($string, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    $string = mb_convert_encoding($string, 'UTF-8', 'auto');
    $string = str_replace("\xc2\xa0", ' ', $string);
    $string = preg_replace("/\r\n?/", "\n", $string);
    $string = preg_replace("/[ \t]+\n/", "\n", $string);
    $string = preg_replace("/\n{3,}/", "\n\n", $string);
    $string = preg_replace('/ {2,}/', ' ', $string);

    if ($removeBreaks) {
        $string = preg_replace('/\s+/', ' ', $string);
    }

    return trim($string);
}

/**
 * Normalize text for SDI / fiscal XML nodes.
 *
 * @param mixed $string
 * @param bool  $removeBreaks
 * @param bool  $preserveDashes
 *
 * @return string
 */
function normalizeSdiText($string, $removeBreaks = false, $preserveDashes = false): string
{
    $string = plainTextFromHtml($string, $removeBreaks);
    if ('' === $string) {
        return '';
    }

    $string = str_replace('€', ' EUR ', $string);
    $string = str_replace('&', ' AND ', $string);
    $string = str_replace('#', ' NUM ', $string);
    $string = str_replace('%', ' PCT ', $string);
    $string = str_replace(array('!', '*', '^', '£', '$', '@', '§', '_', '\\', '/'), ' ', $string);
    $string = str_replace(array('<', '>', '"', "'", '`'), ' ', $string);
    $string = preg_replace('/[´`“”’]/u', ' ', $string);
    $string = preg_replace('/[–]/u', ' ', $string);

    if (! $preserveDashes && ! filter_var($string, FILTER_VALIDATE_EMAIL)) {
        $string = str_replace('-', ' ', $string);
    }

    $string = normalizeSdiAscii($string);

    if ($removeBreaks) {
        $string = preg_replace('/\s+/', ' ', $string);
    }

    $string = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', ' ', $string);
    $string = removeEmoji($string);
    $string = preg_replace($removeBreaks ? '/\s+/' : '/ {2,}/', ' ', $string);

    return trim($string);
}

/**
 * Normalize SDI text to a conservative ASCII subset.
 *
 * @param string $string
 *
 * @return string
 */
function normalizeSdiAscii($string): string
{
    $string = strtr($string, array(
        'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'A',
        'Æ' => 'AE', 'Ç' => 'C', 'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E',
        'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I', 'Ð' => 'D', 'Ñ' => 'N',
        'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O', 'Ø' => 'O',
        'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U', 'Ý' => 'Y', 'Þ' => 'TH',
        'ß' => 'ss', 'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a',
        'å' => 'a', 'æ' => 'ae', 'ç' => 'c', 'è' => 'e', 'é' => 'e', 'ê' => 'e',
        'ë' => 'e', 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', 'ð' => 'd',
        'ñ' => 'n', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o',
        'ø' => 'o', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u', 'ý' => 'y',
        'þ' => 'th', 'ÿ' => 'y', 'Œ' => 'OE', 'œ' => 'oe', 'Š' => 'S', 'š' => 's',
        'Ž' => 'Z', 'ž' => 'z', 'Ł' => 'L', 'ł' => 'l', 'Ą' => 'A', 'ą' => 'a',
        'Ć' => 'C', 'ć' => 'c', 'Ę' => 'E', 'ę' => 'e', 'Ń' => 'N', 'ń' => 'n',
        'Ś' => 'S', 'ś' => 's', 'Ź' => 'Z', 'ź' => 'z', 'Ż' => 'Z', 'ż' => 'z',
    ));

    if (function_exists('remove_accents')) {
        $string = remove_accents($string);
    } elseif (function_exists('iconv')) {
        $normalized = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $string);
        if (false !== $normalized) {
            $string = $normalized;
        }
    }

    return strtr($string, array(
        'Æ' => 'AE',
        'æ' => 'ae',
        'Œ' => 'OE',
        'œ' => 'oe',
        'Ø' => 'O',
        'ø' => 'o',
        'Ð' => 'D',
        'ð' => 'd',
        'Þ' => 'TH',
        'þ' => 'th',
        'ß' => 'ss',
        'Ł' => 'L',
        'ł' => 'l',
    ));
}

/**
 * Normalize text for PEPPOL / UBL business text nodes.
 *
 * @param mixed $string
 * @param bool  $removeBreaks
 *
 * @return string
 */
function normalizePeppolText($string, $removeBreaks = false): string
{
    $string = plainTextFromHtml($string, $removeBreaks);
    if ('' === $string) {
        return '';
    }

    if ($removeBreaks) {
        $string = preg_replace('/\s+/', ' ', $string);
    }

    $string = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', ' ', $string);
    $string = preg_replace($removeBreaks ? '/\s+/' : '/ {2,}/', ' ', $string);

    return trim($string);
}

/**
 * removeEmoji
 *
 * @param $string
 *
 * @return array|mixed|string|string[]|null
 */
function removeEmoji($string)
{
    // Match Enclosed Alphanumeric Supplement
    $regexAlphanumeric = '/[\x{1F100}-\x{1F1FF}]/u';
    $clearString       = preg_replace($regexAlphanumeric, '', $string);

    // Match Miscellaneous Symbols and Pictographs
    $regexSymbols = '/[\x{1F300}-\x{1F5FF}]/u';
    $clearString  = preg_replace($regexSymbols, '', $clearString);

    // Match Emoticons
    $regexEmoticons = '/[\x{1F600}-\x{1F64F}]/u';
    $clearString    = preg_replace($regexEmoticons, '', $clearString);

    // Match Transport And Map Symbols
    $regexTransport = '/[\x{1F680}-\x{1F6FF}]/u';
    $clearString    = preg_replace($regexTransport, '', $clearString);

    // Match Supplemental Symbols and Pictographs
    $regexSupplemental = '/[\x{1F900}-\x{1F9FF}]/u';
    $clearString       = preg_replace($regexSupplemental, '', $clearString);

    // Match Miscellaneous Symbols
    $regexMisc   = '/[\x{2600}-\x{26FF}]/u';
    $clearString = preg_replace($regexMisc, '', $clearString);

    // Match Dingbats
    $regexDingbats = '/[\x{2700}-\x{27BF}]/u';
    $clearString   = preg_replace($regexDingbats, '', $clearString);

    if ('' !== $clearString && is_string($clearString)) {
        $string = $clearString;
    }

    return $string;
}

/**
 * Sanitize function
 *
 * @param $input string|array The string or array to sanitize
 *
 * @return array|bool|string  Sanitized value text or array, otherwise false
 * @since 1.0.0
 *
 */
function sanitize($input)
{
    if (is_string($input)) {
        try {
            return Text::sanitize($input);
        } catch (\Exception $e) {
            echo esc_html__('Input is not a string: ', WC_EL_INV_TEXTDOMAIN) . esc_html($e->getMessage()) . "\n";
        };
    } elseif (is_int($input) || is_numeric($input)) {
        try {
            return strpos($input, '.') !== false ? (float)$input : (int)$input;
        } catch (\Exception $e) {
            echo esc_html__('Input is not a int/float value: ',
                    WC_EL_INV_TEXTDOMAIN) . esc_html($e->getMessage()) . "\n";
        };
    } elseif (is_array($input)) {
        try {
            return Arrays::sanitize($input);
        } catch (\Exception $e) {
            echo esc_html__('Input is not an array: ', WC_EL_INV_TEXTDOMAIN) . esc_html($e->getMessage()) . "\n";
        };
    } else {
        return false;
    }
}

/**
 * WpMl Switch language
 *
 * @since 1.0.0
 */
function switchLang()
{
    if (isWpmlActive() && defined('ICL_LANGUAGE_CODE')) {
        global $sitepress;
        $sitepress->switch_lang(ICL_LANGUAGE_CODE);
    }
}

/**
 * Get current lang
 *
 * @return string
 * @since 1.0.0
 *
 */
function getCurrentLanguage()
{
    if (isWpmlActive() && defined('ICL_LANGUAGE_CODE') && ! empty(ICL_LANGUAGE_CODE)) {
        return strtolower(substr(trim((string) ICL_LANGUAGE_CODE), 0, 2));
    }

    $locale = function_exists('determine_locale') ? determine_locale() : get_locale();
    $locale = trim((string) $locale);
    if ('' === $locale) {
        return '';
    }

    if (preg_match('/^([a-z]{2})[_-]?[A-Z]{0,2}$/i', $locale, $matches)) {
        return strtolower((string) $matches[1]);
    }

    return strtolower(substr($locale, 0, 2));
}

/**
 * Resolve the POP document language override.
 *
 * Priority:
 * 1. Customer country id, when available.
 * 2. WordPress locale mapped to a POP country/language code.
 *
 * @param string|null $customerCountryId
 *
 * @return string|null
 */
function resolveDocumentLanguageOverride(?string $customerCountryId = null): ?string
{
    $customerCountryId = strtoupper(trim((string)$customerCountryId));
    if ('' !== $customerCountryId) {
        return $customerCountryId;
    }

    $locale = (string)get_locale();
    if ('' === $locale) {
        return null;
    }

    if (preg_match('/^[a-z]{2}[_-]([A-Z]{2})$/', $locale, $matches)) {
        return strtoupper((string)$matches[1]);
    }

    $language = strtoupper((string)substr($locale, 0, 2));
    if (preg_match('/^[A-Z]{2}$/', $language)) {
        return $language;
    }

    return null;
}

/**
 * Resolve the current WordPress locale in normalized form.
 *
 * @return string|null
 */
function resolveWpLocaleLanguageCode(): ?string
{
    $locale = trim((string)get_locale());
    if ('' === $locale) {
        return null;
    }

    if (preg_match('/^([a-z]{2})[_-]([A-Z]{2})$/', $locale, $matches)) {
        return strtolower((string)$matches[1]) . '_' . strtoupper((string)$matches[2]);
    }

    if (preg_match('/^[a-z]{2}$/', $locale)) {
        return strtolower($locale);
    }

    return str_replace('-', '_', $locale);
}

/**
 * TODO: add cozmos
 * TODO: use apply_filters and hook from single for-addons.
 *
 * @return mixed|string|void
 */
function getBusinessBillingCountry()
{
    if (isWooCommerceActive()) {
        $shop_location = wc_get_base_location();
        $shop_country  = $shop_location['country'];

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

    if (isPaidMembershipsProActive()) {
        $business_address   = get_option('pmpro_business_address', []);
        $merchantVatCountry = $business_address['billing_country'] ?? '';
        if (! empty($merchantVatCountry)) {
            return $merchantVatCountry;
        }
    }

    return '';
}

/**
 * Send a JSON response back to an Ajax request, indicating failure.
 *
 * If the `$data` parameter is a WP_Error object, the errors
 * within the object are processed and output as an array of error
 * codes and corresponding messages. All other types are output
 * without further processing.
 *
 * @param mixed $data        Data to encode as JSON, then print and die.
 * @param int   $status_code The HTTP status code to output.
 *
 * @since 1.0.0
 *
 */
function sendJsonError($data = null, $status_code = null)
{
    $response = array('success' => false);

    if (isset($data)) {
        if (is_wp_error($data)) {
            $result = array();
            foreach ($data->errors as $code => $messages) {
                foreach ($messages as $message) {
                    $result[] = array('code' => $code, 'message' => $message);
                }
            }

            $response['data'] = $result;
        } else {
            $response['data'] = $data;
        }
    }

    sendJson($response, $status_code);
}

/**≤
 * Send a JSON response back to an Ajax request.
 *
 * @param mixed $response    Variable (usually an array or object) to encode as JSON,
 *                           then print and die.
 * @param int   $status_code The HTTP status code to output.
 *
 * @since 1.0.0
 *
 */
function sendJson($response, $status_code = null)
{
    @header('Content-Type: application/json; charset=' . get_option('blog_charset'));

    if (null !== $status_code) {
        status_header($status_code);
    }

    echo wp_json_encode($response);

    if (wp_doing_ajax()) {
        wp_die('', '', array(
            'response' => null,
        ));
    } else {
        die;
    }
}

/**
 * Get Post Meta
 *
 * @param        $key
 * @param mixed  $default
 * @param int    $post
 * @param bool   $single
 * @param string $type
 * @param string $provider This must be provided. Defaulted to "woocommerce" just to avoid rewriting all the old code
 *
 * @return false|mixed|null
 */
function getPostMeta($key, $default = false, $post = 0, $single = true, $type = 'order', $provider = 'woocommerce')
{
    // Provider support
    $order = \WcElectronInvoice\Providers\OrderQuery::instance()->getProviderOrder($post, $provider);
    if (is_subclass_of($order, '\WcElectronInvoice\Providers\Order') ||
        is_subclass_of($order, '\WcElectronInvoice\Providers\OrderRefund')) {

        return $order->get_meta($key, $single) ?? $default;
    }

    return $default;
}

/**
 * getPostType
 *
 * @param        $postID
 * @param string $type
 *
 * @return false|string|null
 */
function getPostType($postID, $type = 'order')
{
    // WooCommerce Support for High-Performance Order Storage
    if ('order' === $type && class_exists('\Automattic\WooCommerce\Utilities\OrderUtil') &&
        \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled()
    ) {
        return \Automattic\WooCommerce\Utilities\OrderUtil::get_order_type($postID) ?: null;
    } else {
        return get_post_type($postID) ?: null;
    }
}

/**
 * No Key message
 *
 * @param $key
 *
 * @return bool
 * @since 1.0.0
 *
 */
function noKey($key)
{
    if (! $key) {
        return true;
    }

    return false;
}

/**
 * Is Associative Array
 *
 * @param array $array The array to know if is associative or not.
 *
 * @return bool         True if array is associative, false otherwise.
 * @since  1.0.0
 *
 */
function isArrayAssoc(array $array)
{
    return array_keys($array) !== range(0, count($array) - 1);
}

/**
 * Implode Assoc
 *
 * Implode an associative array
 *
 * @param string $glue    The string to use for separate the elements value.
 * @param string $assGlue The string to use to separate key and value.
 * @param array  $arr     The array to implode.
 *
 * @return string|bool The array imploded. False if $arr is not an array. False if $arr is not associative.
 * @since 1.0.0
 *
 */
function implodeAssoc($glue, $assGlue, $arr)
{
    if (wp_is_numeric_array($arr)) {
        return false;
    }

    // The string.
    $string = '';

    foreach ($arr as $k => $v) {
        if (! $v) {
            continue;
        }

        $string .= $k . $assGlue . $v . $glue;
    }

    // Remove the latest glue string.
    $string = rtrim($string, $glue);

    return $string;
}

/**
 * Insert an element into array in a specific position
 *
 * @param array $needle    The array to insert in.
 * @param array $haystack  The array to insert on.
 * @param mixed $pos       The key or index where the array should be inserted.
 * @param bool  $preserve  If the original value and positions should be preserved.
 * @param bool  $recursive To merge recursively or not the result array.
 *
 * @return array            The new merged array
 * @since  1.0.0
 *
 */
function arrayInsertInPos($needle, array &$haystack, $pos, $preserve = false, $recursive = false)
{
    $keys = array_filter(array_intersect(array_keys($needle), array_keys($haystack)), 'is_string');

    if (is_array($pos)) {
        list($key, $before) = $pos;
        $before = (isset($before) && true === $before) ? true : false;
        $pos    = array_search($key, array_keys($haystack), true);
        $pos    = $before ? $pos : $pos + 1;
    }

    if ($keys) {
        if ($preserve) {
            $arr =& $needle;
        } else {
            $arr =& $haystack;
        }

        foreach ($keys as $k => $v) {
            unset($arr[$v]);
        }
    }

    $start    = array_splice($haystack, 0, (int)$pos);
    $func     = $recursive ? 'array_merge_recursive' : 'array_merge';
    $haystack = call_user_func_array($func, array($start, (array)$needle, $haystack));

    return $haystack;
}

/**
 * Merge two arrays.
 *
 * @param array $a1 First array to merge.
 * @param array $a2 Second array to merge.
 *
 * @return array
 * @since 5.5.4
 */
function arrayOverlay($a1, $a2)
{

    foreach ($a1 as $k => $v) {
        if (! array_key_exists($k, $a2)) {
            continue;
        }
        if (is_array($v) && is_array($a2[$k])) {
            $a1[$k] = wc_array_overlay($v, $a2[$k]);
        } else {
            $a1[$k] = $a2[$k];
        }
    }

    return $a1;
}

/**
 * User to sort checkout fields based on priority with uasort.
 *
 * @param array $a First field to compare.
 * @param array $b Second field to compare.
 *
 * @return int
 * @since 5.5.4
 */
function checkoutFieldsUasortComparison($a, $b)
{
    /*
     * We are not guaranteed to get a priority
     * setting. So don't compare if they don't
     * exist.
     */
    if (! isset($a['priority'], $b['priority'])) {
        return 0;
    }

    return uasortComparison($a['priority'], $b['priority']);
}

/**
 * User to sort two values with ausort.
 *
 * @param int $a First value to compare.
 * @param int $b Second value to compare.
 *
 * @return int
 * @since 5.5.4
 */
function uasortComparison($a, $b)
{
    if ($a === $b) {
        return 0;
    }

    return ($a < $b) ? -1 : 1;
}

/**
 * showAdminNotice
 *
 * @param $messageText
 * @param $type
 * @param $notice_class
 * @param $hook
 *
 * @return void
 */
function showAdminNotice(
    $messageText,
    $type = 'updated',
    $notice_class = 'pop-notice',
    $hook = 'admin_notices',
    $html = false
) {
    $message = '<div class="notice ' . esc_attr($notice_class) . ' notice-' . esc_attr($type) . '  is-dismissible">';
    if (! $html) {
        $messageText = esc_html($messageText);
    }

    $messageText = wp_kses($messageText, ['b' => [], 'br' => [], 'u' => [], 'span' => ['class' => true]]);
    $message     .= '<p>' . $messageText . '</p>';
    $message     .= '</div>';

    if ($hook) {
        add_action($hook, function () use ($message, $type, $notice_class) {
            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
            echo $message;
        });
    } else {
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
        echo $message;
    }
}

/**
 * pluginNotice
 *
 * @return void
 * @since 1.0.0
 *
 */
function pluginNotice()
{
    if (! haveAnActiveProvider()) :
        $message = sprintf('<b>%s</b><br>%s', esc_html__(
            'POP – Electronic Invoices & Legal Documents Generator for eCommerce (ex-POP)',
            WC_EL_INV_TEXTDOMAIN
        ), esc_html__(
            'Warning: there is no active provider, the plugin needs a provider to work.',
            WC_EL_INV_TEXTDOMAIN
        ));
        showAdminNotice($message, 'error', 'pop-notice', 'admin_notices', true);
    endif;
}

/**
 * Helper for get Wc Order class name
 *
 * @param $obj
 * @param $classname
 *
 * @return string
 *
 * @since 2.2
 * - Support for WC Admin package
 *
 */
function wcOrderClassName($obj, $classname)
{
    if (isWooCommerceActive() && \WC()->version >= '4.0.1') {
        if ('\WC_Order' === $classname) {
            if ($obj instanceof \WC_Order) {
                return '\WC_Order';
            } elseif ($obj instanceof \Automattic\WooCommerce\Admin\Overrides\Order) {
                return '\Automattic\WooCommerce\Admin\Overrides\Order';
            }
        } elseif ('\WC_Order_Refund' === $classname) {
            if ($obj instanceof \WC_Order_Refund) {
                return '\WC_Order_Refund';
            } elseif ($obj instanceof \Automattic\WooCommerce\Admin\Overrides\OrderRefund) {
                return '\Automattic\WooCommerce\Admin\Overrides\OrderRefund';
            }
        } else {
            return $classname;
        }
    }

    return $classname;
}

/**
 * getNoTaxProductItemsTotals
 *
 * @param $items
 *
 * @return float
 * @since 4.5.4
 */
function getNoTaxProductItemsTotals($items)
{
    $totalsNoTaxProduct = 0;

    if (! empty($items)) {
        foreach ($items as $item) {
            if (0 === (int)$item['total_tax']) {
                $totalsNoTaxProduct += floatval($item['total']);
            }
        }
    }

    return floatval($totalsNoTaxProduct);
}

/**
 * Get customer
 *
 * @param $customerID
 *
 * @return bool|null
 */
function setCustomerLocation($customerID)
{
    if (! isWooCommerceActive()) {
        return null;
    }

    $parse = ! empty($_SERVER) ? parse_url($_SERVER['REQUEST_URI']) : array();
    if (! empty($parse) && isset($parse['path'])) {
        $path = explode('/', $parse['path']);
        $path = array_filter($path);
        if (! in_array('wc-inv', $path, true) &&
            ! in_array('wp-admin', $path, true)
        ) {
            return false;
        }
    }

    try {
        // Init customer by ID
        $initCustomer = new \WC_Customer($customerID);

        add_filter('woocommerce_get_tax_location', function ($location, $taxClass, $customer) use ($initCustomer) {
            if ($initCustomer instanceof \WC_Customer) {
                $location = array(
                    $initCustomer->get_billing_country(),
                    $initCustomer->get_billing_state(),
                    $initCustomer->get_billing_postcode(),
                    $initCustomer->get_billing_city(),
                );
            }

            return $location;
        }, 3, 20);

        return true;
    } catch (\Exception $e) {
        return null;
    }
}

/**
 * Payment Method Code info
 *
 * @param $order
 *
 * @return mixed|string|void
 * @since 4.1.5
 *
 */
function paymentMethodCode($order, $refundCheck = true)
{
    if (! property_exists($order, 'payment_method') &&
        ! property_exists($order, 'refunded') &&
        ! isset($order->refunded['refunded_payment_method'])
    ) {
        return '';
    }

    /**
     * Payment method for refunded:
     */
    if ('shop_order_refund' === $order->order_type &&
        isset($order->refunded['refunded_payment_method'])
    ) {
        if ('' === $order->refunded['refunded_payment_method']) {
            return sprintf('<b style="color:#007cba;">MP01</b> - %s', esc_html__('Cash', WC_EL_INV_TEXTDOMAIN));
        }
        switch ($order->refunded['refunded_payment_method']) {
            default:
            case 'MP01':
                return sprintf('<b style="color:#007cba;">MP01</b> - %s',
                    esc_html__('Cash', WC_EL_INV_TEXTDOMAIN));
            case 'MP02':
                return sprintf('<b style="color:#007cba;">MP02</b> - %s',
                    esc_html__('Bank cheque', WC_EL_INV_TEXTDOMAIN));
            case 'MP03':
                return sprintf('<b style="color:#007cba;">MP03</b> - %s',
                    esc_html__('Circular check', WC_EL_INV_TEXTDOMAIN));
            case 'MP05':
                return sprintf('<b style="color:#007cba;">MP05</b> - %s',
                    esc_html__('Wire transfer', WC_EL_INV_TEXTDOMAIN));
            case 'MP08':
                return sprintf('<b style="color:#007cba;">MP08</b> - %s',
                    esc_html__('Credit card', WC_EL_INV_TEXTDOMAIN));
        }
    }

    $payment_method = null;
    $methodTitle    = null;
    $code           = null;
    if (property_exists($order, 'payment_method')) {
        $payment_method = false !== strpos($order->payment_method, 'stripe') ? 'stripe' : $payment_method;
        $payment_method = false !== strpos($order->payment_method, 'paypal') ? 'paypal' : $payment_method;
    }

    if (property_exists($order, 'payment_method')) {
        if (! $payment_method) {
            $payment_method = $order->payment_method;
        }

        if (property_exists($order, 'payment_method_title')) {
            $methodTitle = $order->payment_method_title;
        }

        if (empty($methodTitle) && $payment_method === 'cod') {
            $methodTitle = esc_html__('Cash', WC_EL_INV_TEXTDOMAIN);
        }

        if ($payment_method) {
            $code = getPaymentMethodInvoiceCode($payment_method);
        }
    }

    if ($payment_method && $methodTitle && $code) {
        return apply_filters('wc_el_inv-default_payment_method_invoice_table',
            sprintf('<b style="color:#007cba;">%s</b> - %s', $code, $methodTitle),
            $code, $payment_method
        );
    }
}

/**
 * getPaymentMethodInvoiceCode
 *
 * @param $payment_method
 *
 * @return string
 */
function getPaymentMethodInvoiceCode($payment_method)
{
    $credit_card_methods = [
        'paypal',
        'ppec_paypal',
        'ppcp',
        'ppcp-gateway',
        'ppcp-card-button-gateway',
        'ppcp-credit-card-gateway',
        'scalapay',
        'wc-scalapay-payin3',
        'wc-scalapay-payin4',
        'woocommerce_payments',
        'stripe',
        'stripe_cc',
        'xpay',
        'soisy',
        'igfs',
        'revolut_cc',
    ];

    $method_code_map = [
        'cod'         => 'MP01',
        'bacs'        => 'MP05',
        '18app'       => 'MP05',
        'docente'     => 'MP05',
        'cheque'      => 'MP02',
        'stripe_sepa' => 'MP19',
    ];

    if (isset($method_code_map[$payment_method])) {
        $code = $method_code_map[$payment_method];
    } elseif (in_array($payment_method, $credit_card_methods)) {
        $code = 'MP08';
    } else {
        // fallback
        $code = 'MP08';
    }

    return apply_filters('wc_el_inv-payment_method-invoice-code', $code, $payment_method);
}

/**
 * invoicePaymentMethodOrderCode
 *
 * @param $order
 *
 * @return mixed|string|void|null
 */
function invoicePaymentMethodOrderCode($order)
{
    $payment_method = null;

    if (is_int($order)) {
        $order = wc_get_order($order);
    }

    if (method_exists($order, 'get_payment_method')) {
        $payment_method = $order->get_payment_method();
        $payment_method = false !== strpos($payment_method, 'stripe') ? 'stripe' : $payment_method;
        $payment_method = false !== strpos($payment_method, 'paypal') ? 'paypal' : $payment_method;
    }

    /**
     * Payment method for order:
     *
     * - Bacs
     * - Cheque
     * - PayPal Express Checkout
     * - Stripe
     * - Stripe SEPA
     * - ..... etc
     */
    if (method_exists($order, 'get_payment_method')) {
        if (! $payment_method) {
            $payment_method = $order->get_payment_method();
        }

        $code = getPaymentMethodInvoiceCode($payment_method);

        return apply_filters('wc_el_inv-invoice_default_payment_method', $code, $payment_method);
    }
}

/**
 * checkOptions
 *
 * @param $type
 *
 * @return bool true field is isset, false field is empty (verification)
 */
function checkOptions($type = null): bool
{
    if (! $type) {
        return false;
    }

    $required = [
        get_option("wc_el_inv-general_store_country", false),
        get_option("wc_el_inv-general_store_your_name", false),
        get_option("wc_el_inv-general_store_your_surname", false),
        get_option("wc_el_inv-general_store_company_name", false),
        get_option("wc_el_inv-general_store_vat_number", false),
        get_option("wc_el_inv-general_store_tax_regime", false),
    ];

    if ('general' === $type &&
        (in_array(false, $required, true) && in_array('', $required, true))
    ) {
        return false;
    }

    if ('numeration' === $type) {
        if (class_exists('WooPoPToFattureInCloud\Plugin')) {
            return true;
        }

        if (! boolval(OptionPage::init()->getOptions('number_next_invoice'))) {
            OptionPage::init()->setOption('number_next_invoice', 1);

            return true;
        }
    }

    if ('checkout' === $type &&
        ! boolval(OptionPage::init()->getOptions('invoice_required'))
    ) {
        OptionPage::init()->setOption('invoice_required', 'required');

        return true;
    }

    return true;
}

/**
 * getN4RefNormative
 *
 * @param $get
 *
 * @return array|bool|mixed|string
 */
function getN4RefNormative($get = 'name')
{
    $default = null; // key 999
    $value   = OptionPage::init()->getOptions('invoice_ref_normative_N4_nature') ?: $default;

    // No selection
    if (! $value || "999" === $value) {
        return null;
    }

    $options = array(
        '19' => esc_html__("Exempt VAT pursuant to art.10 of D.P.R 633/1972", WC_EL_INV_TEXTDOMAIN),
        '24' => esc_html__("Exempt VAT pursuant to art. 10, paragraph 1, no. 18 of D.P.R. 633/1972",
            WC_EL_INV_TEXTDOMAIN),
        '25' => esc_html__("Exempt VAT pursuant to art. 10, paragraph 20 of D.P.R. 633/72", WC_EL_INV_TEXTDOMAIN),
        '27' => esc_html__("Exempt VAT pursuant to art.10 no.27 of D.P.R 633/1972", WC_EL_INV_TEXTDOMAIN),
        '37' => esc_html__("Exempt VAT pursuant to art. 10, paragraph 9 of D.P.R. 633/72", WC_EL_INV_TEXTDOMAIN),
        '45' => esc_html__("Non taxable pursuant to art. 9", WC_EL_INV_TEXTDOMAIN),
    );

    if ('name' === $get) {
        return $options[$value];
    }

    return $value;
}

/**
 * getN5RefNormative
 *
 * @param $get
 *
 * @return array|bool|mixed|string
 */
function getN5RefNormative()
{
    $default = "Escluso Art.74 ter D.P.R. 633/72";

    return OptionPage::init()->getOptions('invoice_ref_normative_N5_nature') ?: $default;
}

/**
 * isXml
 *
 * @param $string
 *
 * @return object
 */
function isXml($string): object
{
    $prev = libxml_use_internal_errors(true);

    if (! is_string($string)) {
        return (object)array(
            'valid'      => false,
            'xml'        => false,
            'xml_string' => false,
            'errors'     => ['error' => 'param string is array', 'string' => print_r($string, true)],
        );
    }

    $docXml = simplexml_load_string($string);
    $errors = libxml_get_errors();
    libxml_clear_errors();
    libxml_use_internal_errors($prev);

    $valid = false !== $docXml && empty($errors);

    return (object)array(
        'valid'      => $valid,
        'xml'        => $docXml,
        'xml_string' => $string,
        'errors'     => $errors,
    );
}

/**
 * getXmlFromOrder
 *
 * @param $order
 *
 * @info The request is finalized with an api call
 *
 * @return false|string
 */
function getXmlFromOrder($order)
{
    if (! is_subclass_of($order, '\WcElectronInvoice\Providers\Order') &&
        ! is_subclass_of($order, '\WcElectronInvoice\Providers\OrderRefund')
    ) {
        return false;
    }

    $orderProviderClass = null;
    if (is_subclass_of($order, '\WcElectronInvoice\Providers\Order')) {
        $orderProviderClass = '\WcElectronInvoice\Providers\Order';
    } elseif (is_subclass_of($order, '\WcElectronInvoice\Providers\OrderRefund')) {
        $orderProviderClass = '\WcElectronInvoice\Providers\OrderRefund';
    }

    $order = resolveProviderDocument(
        $order->id,
        $order->get_provider(),
        is_subclass_of($order, '\WcElectronInvoice\Providers\OrderRefund') ? 'shop_order_refund' : 'shop_order',
        method_exists($order, 'get_parent_id') ? $order->get_parent_id() : 0
    );
    if (! $order) {
        return false;
    }

    // I check and update the formatted number
    checkAndUpdateFormattedNumber($order);

    $buildXml = new \WcElectronInvoice\Xml\BuildXml();
    $buildXml->singleOrderXmlData($order, $orderProviderClass);
    $createXml = new CreateXml(new \SimpleXMLElement(
            '<?xml version="1.0" encoding="UTF-8"?><xmlns:p:FatturaElettronica/>',
            LIBXML_NOERROR,
            false,
            'p',
            true)
    );

    if ((defined('DOING_AJAX') && DOING_AJAX || defined('DOING_CRON') && DOING_CRON)) {
        $action = \WcElectronInvoice\Functions\filterInput($_POST, 'action') ?? null;
        $method = \WcElectronInvoice\Functions\filterInput($_POST, 'method') ?? null;
        if (
            (
                'createAndSendInvoice' === $action
                || 'createAndSendUblInvoice' === $action
                || 'createAndSendKsefInvoice' === $action
                || 'createAndSendZohoInvoice' === $action
            ) &&
            \WcElectronInvoice\Functions\isBackgroundExecutionMode($method, ['cronjob', 'ajax'])
        ) {
            return $createXml->create($buildXml->xmlData);
        }
    }

    // Create xml
    ob_start();
    $createXml->create($buildXml->xmlData);

    return ob_get_clean();
}

/**
 * Return the requested document type for endpoint-style requests.
 *
 * @param string $default
 *
 * @return string
 */
function getRequestedProviderDocumentType(string $default = 'shop_order'): string
{
    $documentType = filterInput($_GET, 'document_type', FILTER_UNSAFE_RAW);
    $documentType = is_string($documentType) ? sanitize_key($documentType) : '';

    if ('shop_order_refund' === $documentType) {
        return 'shop_order_refund';
    }

    return $default;
}

/**
 * Resolve a provider document by id/provider/type.
 *
 * This is required for PMPro refunds because order and refund share the same
 * source order id but must still be addressable as distinct document entities.
 *
 * @param int|string $objectId
 * @param string     $provider
 * @param string     $documentType
 * @param int|string $parentId
 *
 * @return \WcElectronInvoice\Providers\OrderAbstract|null
 */
function resolveProviderDocument($objectId, $provider, string $documentType = 'shop_order', $parentId = 0)
{
    $provider = sanitize_key((string)$provider);
    $objectId = (int)$objectId;
    $parentId = (int)$parentId;
    if ($objectId < 1 || '' === $provider) {
        return null;
    }

    $order = OrderQuery::instance()->getProviderOrder($objectId, $provider);
    if (! $order) {
        return null;
    }

    if ('shop_order_refund' !== $documentType) {
        return $order;
    }

    if (is_subclass_of($order, '\WcElectronInvoice\Providers\OrderRefund')) {
        return $order;
    }

    if (! method_exists($order, 'get_refunds')) {
        return null;
    }

    $refunds = $order->get_refunds();
    if (! is_array($refunds) || empty($refunds)) {
        return null;
    }

    foreach ($refunds as $refund) {
        if (! is_subclass_of($refund, '\WcElectronInvoice\Providers\OrderRefund')) {
            continue;
        }

        if ($parentId > 0 && method_exists($refund, 'get_parent_id') && (int)$refund->get_parent_id() !== $parentId) {
            continue;
        }

        return $refund;
    }

    return null;
}

/**
 * removeProductTotalZeroOption
 *
 * @return bool
 */
function removeProductTotalZeroOption()
{
    $bool                   = false;
    $removeProductTotalZero = OptionPage::init()->getOptions('remove_product_from_invoice_is_total_zero');
    if ('on' === $removeProductTotalZero) {
        $bool = true;
    }

    return $bool;
}

/**
 * multiCurrencyConversion
 *
 * @param $amount
 * @param $from
 * @param $to
 *
 * @return float|string
 */
function multiCurrencyConversion($amount, $from, $to)
{
    if (! function_exists('WC_Payments_Multi_Currency')) {
        // return current amount.
        return (float)$amount;
    }

    if ('EUR' === $from) {
        // return current amount.
        return (float)$amount;
    }

    if (is_string($amount)) {
        $amount = (float)$amount;
    }

    try {
        // return currency conversion amount.
        return WC_Payments_Multi_Currency()->get_raw_conversion($amount, $to, $from);
    } catch (\WCPay\MultiCurrency\Exceptions\InvalidCurrencyException $ic) {
        log('\WCPay\MultiCurrency\Exceptions\InvalidCurrencyException: ' . $ic->getMessage());

        // return current amount.
        return (float)$amount;
    } catch (\WCPay\MultiCurrency\Exceptions\InvalidCurrencyRateException $ir) {
        log('\WCPay\MultiCurrency\Exceptions\InvalidCurrencyRateException: ' . $ir->getMessage());

        // return current amount.
        return (float)$amount;
    }
}

/**
 * Log
 *
 * @param $message
 */
function log($message)
{
    if (defined('WP_DEBUG') && WP_DEBUG) {
        $message = WC_EL_INV_NAME . "\n{$message}\n# ------------------------------------------- #\n";
        error_log($message);
    }
}

/**
 * getDiagnosticLogStatusOptionKey
 *
 * @param string $logKey
 *
 * @return string
 */
function getDiagnosticLogStatusOptionKey($logKey)
{
    return 'wc_el_inv_log_status_' . sanitize_key((string)$logKey);
}

/**
 * normalizeDiagnosticLogConfig
 *
 * @param array $args
 *
 * @return array
 */
function normalizeDiagnosticLogConfig(array $args)
{
    $contentDir = defined('WP_CONTENT_DIR') ? WP_CONTENT_DIR : trailingslashit(ABSPATH) . 'wp-content';
    $logKey     = sanitize_key($args['key'] ?? $args['log_dir_slug'] ?? '');
    $dirSlug    = sanitize_key($args['log_dir_slug'] ?? $logKey);
    $fileName   = sanitize_file_name($args['log_file_name'] ?? 'debug.log');
    $capability = (string)($args['capability'] ?? 'manage_options');
    $pluginName = (string)($args['plugin_name'] ?? (defined('WC_EL_INV_NAME') ? WC_EL_INV_NAME : 'POP'));

    return array(
        'key'          => $logKey,
        'enabled'      => ! empty($args['enabled']),
        'plugin_name'  => $pluginName,
        'capability'   => $capability,
        'log_dir_slug' => $dirSlug,
        'log_file_name'=> $fileName,
        'dir_path'     => trailingslashit($contentDir) . $dirSlug . '/',
        'file_path'    => trailingslashit($contentDir) . $dirSlug . '/' . $fileName,
    );
}

/**
 * setDiagnosticLogStatus
 *
 * @param array  $config
 * @param string $message
 *
 * @return void
 */
function setDiagnosticLogStatus(array $config, $message)
{
    update_option(getDiagnosticLogStatusOptionKey($config['key']), array(
        'message'    => (string)$message,
        'updated_at' => time(),
    ));
}

/**
 * clearDiagnosticLogStatus
 *
 * @param array $config
 *
 * @return void
 */
function clearDiagnosticLogStatus(array $config)
{
    update_option(getDiagnosticLogStatusOptionKey($config['key']), array());
}

/**
 * getDiagnosticLogStatus
 *
 * @param array $config
 *
 * @return array
 */
function getDiagnosticLogStatus(array $config)
{
    $status = get_option(getDiagnosticLogStatusOptionKey($config['key']), array());

    return is_array($status) ? $status : array();
}

/**
 * prepareDiagnosticLog
 *
 * @param array $args
 * @param bool  $createMissing
 *
 * @return array
 */
function prepareDiagnosticLog(array $args, $createMissing = true)
{
    $config = normalizeDiagnosticLogConfig($args);
    $status = array(
        'key'           => $config['key'],
        'enabled'       => $config['enabled'],
        'dir_path'      => $config['dir_path'],
        'file_path'     => $config['file_path'],
        'dir_exists'    => is_dir($config['dir_path']),
        'file_exists'   => file_exists($config['file_path']),
        'dir_writable'  => false,
        'file_writable' => false,
        'ready'         => false,
        'error'         => '',
    );

    $storedStatus = getDiagnosticLogStatus($config);
    if (! empty($storedStatus['message'])) {
        $status['error'] = (string)$storedStatus['message'];
    }

    if (! $config['enabled']) {
        return $status;
    }

    if (! $status['dir_exists'] && $createMissing) {
        $created = wp_mkdir_p($config['dir_path']);
        if (! $created) {
            $created = @mkdir($config['dir_path'], 0755, true);
        }

        if (! $created) {
            $status['error'] = "Unable to create log directory: {$config['dir_path']}";
            setDiagnosticLogStatus($config, $status['error']);

            return $status;
        }

        $status['dir_exists'] = true;
    }

    $status['dir_writable'] = $status['dir_exists'] && is_writable($config['dir_path']);
    if (! $status['dir_writable']) {
        $status['error'] = "Log directory is not writable: {$config['dir_path']}";
        setDiagnosticLogStatus($config, $status['error']);

        return $status;
    }

    if (! $status['file_exists'] && $createMissing) {
        $handle = @fopen($config['file_path'], 'a');
        if (false !== $handle) {
            fclose($handle);
            $status['file_exists'] = true;
        }
    }

    $status['file_exists']   = file_exists($config['file_path']);
    $status['file_writable'] = $status['file_exists'] ? is_writable($config['file_path']) : $status['dir_writable'];

    if (! $status['file_exists']) {
        $status['error'] = "Unable to create log file: {$config['file_path']}";
        setDiagnosticLogStatus($config, $status['error']);

        return $status;
    }

    if (! $status['file_writable']) {
        $status['error'] = "Log file is not writable: {$config['file_path']}";
        setDiagnosticLogStatus($config, $status['error']);

        return $status;
    }

    $status['ready'] = true;
    $status['error'] = '';
    clearDiagnosticLogStatus($config);

    return $status;
}

/**
 * writeDiagnosticLog
 *
 * @param string $message
 * @param array  $args
 *
 * @return bool
 */
function writeDiagnosticLog($message, array $args)
{
    $config = normalizeDiagnosticLogConfig($args);

    if (! $config['enabled']) {
        return false;
    }

    $status = prepareDiagnosticLog($config, true);
    if (! $status['ready']) {
        if (! empty($status['error'])) {
            error_log($status['error']);
        }

        return false;
    }

    $fullMessage = $config['plugin_name'] . "\n" . trim((string)$message) . "\n# ------------------------------------------- #\n";
    $logEntry    = '[' . date_i18n('d/m/Y H:i') . '] - ' . $fullMessage . PHP_EOL;
    $written     = error_log($logEntry, 3, $status['file_path']);

    if (! $written) {
        $errorMessage = "Unable to write log file: {$status['file_path']}";
        setDiagnosticLogStatus($config, $errorMessage);
        error_log($errorMessage);

        return false;
    }

    clearDiagnosticLogStatus($config);

    return true;
}

/**
 * getDiagnosticLogRegistry
 *
 * @return array
 */
function getDiagnosticLogRegistry()
{
    $logs       = apply_filters('wc_el_inv-diagnostic_logs', array());
    $normalized = array();

    foreach ($logs as $key => $config) {
        if (! is_array($config)) {
            continue;
        }

        if (empty($config['key']) && is_string($key)) {
            $config['key'] = $key;
        }

        $config = normalizeDiagnosticLogConfig($config);
        if (empty($config['key'])) {
            continue;
        }

        $normalized[$config['key']] = $config;
    }

    return $normalized;
}

/**
 * getDiagnosticLogViewerUrl
 *
 * @param string $logKey
 *
 * @return string
 */
function getDiagnosticLogViewerUrl($logKey)
{
    $logKey = sanitize_key((string)$logKey);
    $url    = admin_url('admin-post.php?action=wc_el_inv_view_log&log_key=' . rawurlencode($logKey));

    return wp_nonce_url($url, 'wc_el_inv_view_log_' . $logKey);
}

/**
 * viewDiagnosticLog
 *
 * @return void
 */
function viewDiagnosticLog()
{
    $logKey   = sanitize_key(filterInput($_GET, 'log_key', FILTER_UNSAFE_RAW));
    $registry = getDiagnosticLogRegistry();

    if (! $logKey || empty($registry[$logKey])) {
        wp_die('Log not found', 'Log not found', array('response' => 404));
    }

    if (! current_user_can($registry[$logKey]['capability'])) {
        wp_die('You do not have access to this log', 'Access denied', array('response' => 403));
    }

    $nonce = filterInput($_GET, '_wpnonce', FILTER_UNSAFE_RAW);
    if (! $nonce || ! wp_verify_nonce($nonce, 'wc_el_inv_view_log_' . $logKey)) {
        wp_die('Invalid log request', 'Access denied', array('response' => 403));
    }

    $status = prepareDiagnosticLog($registry[$logKey], true);
    if (! $status['ready'] || ! is_readable($status['file_path'])) {
        $message = ! empty($status['error']) ? $status['error'] : 'Unable to read log file';
        wp_die($message, 'Log unavailable', array('response' => 500));
    }

    $contentDir = defined('WP_CONTENT_DIR') ? WP_CONTENT_DIR : trailingslashit(ABSPATH) . 'wp-content';
    $rootPath   = realpath($contentDir);
    $filePath   = realpath($status['file_path']);

    if (! $rootPath || ! $filePath || strpos($filePath, $rootPath) !== 0) {
        wp_die('You do not have access to this log', 'Access denied', array('response' => 403));
    }

    nocache_headers();
    header('Content-Type: text/plain; charset=' . get_option('blog_charset'));
    header('Content-Disposition: inline; filename="' . sanitize_file_name(basename($filePath)) . '"');
    header('Content-Length: ' . filesize($filePath));
    readfile($filePath);
    exit;
}

/**
 * Wrapper for mb_strtoupper which see's if supported first.
 *
 * @param string $string String to format.
 *
 * @return string
 * @since  5.5.4
 */
function strtoupper($string)
{
    $string = $string ?? '';

    return function_exists('mb_strtoupper') ? mb_strtoupper($string) : strtoupper($string);
}

/**
 * getSessionCookieArgs
 *
 * @param string $accept
 * @param string $location
 *
 * @return array
 */
function getSessionCookieArgs(string $accept = 'application/pdf', string $location = 'admin'): array
{
    $cookies = array();
    $args    = array();

    if ('front' === $location && 'application/pdf' === $accept) {
        foreach ($_COOKIE as $name => $value) {
            if (stripos($name, 'wordpress_logged_in') !== false || stripos($name, 'wp_woocommerce_session') !== false) {
                $cookies[] = new \WP_Http_Cookie(['name' => $name, 'value' => $value]);
            }
        }
    } else {
        foreach ($_COOKIE as $name => $value) {
            $cookies[] = new \WP_Http_Cookie(['name' => $name, 'value' => $value]);
        }
    }

    if (! empty($cookies)) {
        $args = [
            'timeout' => 15,
            'cookies' => $cookies,
            'headers' => [
                'Accept' => $accept,
            ],
        ];
    }

    return $args;
}

/**
 * getListKeyValue
 *
 * @param $list
 * @param $key_field
 * @param $value_field
 *
 * @return array
 */
function getListKeyValue($list, $key_field, $value_field)
{
    $result = [];
    if (is_array($list) && ! empty($list)) {
        foreach ($list as $item) {
            $key_name = $key_field;

            if ($key_field === 'item_code' && ! isset($item[$key_field]) && isset($item['code'])) {
                $key_name = 'code';
            }

            if (isset($item[$key_name]) && isset($item[$value_field])) {
                $result[$item[$key_name]] = $item[$value_field];
            }
        }
    }

    return $result;
}

/**
 * getActiveAddon
 *
 * @param $code
 * @param $data
 *
 * @return bool
 */
function getActiveAddon($code, $data)
{
    if ('dev' === WC_EL_INV_ENV) {
        return true;
    }

    if (! empty($code) && in_array($code, array_keys(Integrations::$addonListCode), true)) {
        if (array_key_exists($code, $data)) {
            return ($data[$code] === 'active');
        }
    }

    return false;
}

/**
 * POP notifications
 *
 * @return false|string|void
 */
function notifications()
{
    $transient_name = 'pop_admin_notifications';
    $data           = get_transient($transient_name);

    if ($data === false || 'dev' === WC_EL_INV_ENV) {
        $response = wp_remote_get(WC_EL_INV_POP_NOTIFICATIONS_JSON, ['timeout' => 3]);

        if (is_wp_error($response)) {
            return;
        }

        $responseCode = (int)wp_remote_retrieve_response_code($response);
        if (($responseCode < 200 || $responseCode >= 300)) {
            return;
        }

        $data = json_decode(wp_remote_retrieve_body($response), true);
        if (json_last_error() !== JSON_ERROR_NONE || ! is_array($data)) {
            return;
        }

        set_transient($transient_name, $data, HOUR_IN_SECONDS * 12);
    }

    $data = array_filter($data);
    foreach ($data as $notification) {
        if (is_array($notification) &&
            isset($notification['name']) &&
            isset($notification['title']) &&
            ( ! empty($notification['show_if']) ||
              ! empty($notification['hide_if']) ||
              ( ! empty($notification['starts']) || ! empty($notification['ends']) )
            )
        ) {
            switch ($notification['type']) {
                case 'general':
                default:
                    $type_class = 'notice-info';
                    break;
                case 'error':
                    $type_class = 'notice-error';
                    break;
            }

            $current_date = date('Y-m-d');
            $start_date   = $notification['starts'] ?? '';
            $end_date     = $notification['ends'] ?? '';

            $show_by_date = true;
            if (! empty($start_date) && $current_date < $start_date) {
                $show_by_date = false;
            }
            if (! empty($end_date) && $current_date > $end_date) {
                $show_by_date = false;
            }

            if (! $show_by_date) {
                continue;
            }
            $conditions_met = true;
            if (! empty($notification['show_if'])) {
                $show_if = array_filter($notification['show_if']);
                foreach ($show_if as $key => $condition) {
                    if (! evaluateCondition([$key, ...$condition])) {
                        $conditions_met = false;
                        break;
                    }
                }
            }

            if (! empty($notification['hide_if'])) {
                $hide_if = array_filter($notification['hide_if']);
                foreach ($hide_if as $key => $condition) {
                    if (evaluateCondition([$key, ...$condition])) {
                        $conditions_met = false;
                        break;
                    }
                }
            }

            if (! $conditions_met) {
                continue;
            }

            $dismissable = $notification['dismissable'] ? 'is-dismissible' : '';
            $dashicon    = $notification['dashicon'] ? '<span class="dashicons dashicons-' . $notification['dashicon'] . '"></span>' : '';

            return sprintf('<div class="pop_notifications notice %s %s priority_%s %s"><h3>%s %s</h3>%s</div>',
                esc_attr($type_class),
                esc_attr($dismissable),
                esc_attr($notification['priority']),
                esc_html($notification['name']),
                $dashicon,
                esc_html($notification['title']),
                $notification['content'],
            );
        } else {
            return false;
        }
    }
}

/**
 * Resolve the current POP platform identifier used by notifications conditions.
 *
 * @return string
 */
function getCurrentPlatform(): string
{
    $platform = apply_filters('wc_el_inv-current_platform', 'wordpress');
    if (! is_string($platform) || '' === trim($platform)) {
        return 'wordpress';
    }

    return sanitize_key($platform);
}

/**
 * evaluateCondition
 *
 * @param $condition
 *
 * @return bool|int
 */
function evaluateCondition($condition)
{
    if (empty($condition) || ! is_array($condition)) {
        return false;
    }

    $key      = $condition[0] ?? null;
    $operator = $condition[2] ?? null;
    $value    = $condition[3] ?? null;

    if (! $key || ! $operator || $value === null) {
        return false;
    }

    $value = 'empty' === $value || 'null' === $value || 'undefined' === $value ? '' : $value;

    switch ($key) {
        case 'platform_is':
            $platform = getCurrentPlatform();

            return compareValues($platform, $operator, sanitize_key((string)$value));
        case 'check_plugin_version':
        case 'plugins_active':
            $plugin_file = $condition[1];
            $is_active   = is_plugin_active($plugin_file);
            if ($is_active) {
                $current_version = get_plugin_data(WP_PLUGIN_DIR . '/' . $plugin_file)['Version'] ?? null;
                if ($current_version) {
                    return compareValues($current_version, $operator, $value);
                }
            }

            return false;
        case 'pop_license':
            $license_value = OptionPage::init()->getOptions('license_key');

            return compareValues($license_value, $operator, $value);

        default:
            return false;
    }
}

/**
 * compareValues
 *
 * @param $a
 * @param $operator
 * @param $b
 *
 * @return bool|int
 */
function compareValues($a, $operator, $b)
{
    switch ($operator) {
        case '=':
        case '==':
            return $a == $b;
        case '===':
            return $a === $b;
        case '!=':
            return $a != $b;
        case '!==':
            return $a !== $b;
        case '<':
            return version_compare($a, $b, '<');
        case '<=':
            return version_compare($a, $b, '<=');
        case '>':
            return version_compare($a, $b, '>');
        case '>=':
            return version_compare($a, $b, '>=');
        default:
            return false;
    }
}

/**
 * containsNumber
 *
 * @param $string
 * @param $number
 *
 * @return bool
 */
function containsNumber($string, $number)
{
    $number = (string)$number;

    $pattern = '/(^|[^0-9])0*' . preg_quote($number, '/') . '([^0-9]|$)/';

    return preg_match($pattern, $string) === 1;
}

/**
 * returnLocalFile
 *
 * @param $data
 * @param $returnFile
 *
 * @return false|string|void
 */
function returnLocalFile($data, $returnFile)
{
    if (! apply_filters('pop_use_local_cache', true)) {
        return;
    }

    $objID     = isset($data['id']) ? intval($data['id']) : 0;
    $provider  = isset($data['order_provider']) ? sanitize_text_field($data['order_provider']) : '';
    $view      = isset($data['view']) && $data['view'];
    $save      = isset($data['save']) && $data['save'];
    $save_bulk = isset($data['save_bulk']) && $data['save_bulk'];
    $type      = isset($data['document_type']) ? sanitize_key((string)$data['document_type']) : getRequestedProviderDocumentType();
    $parentId  = isset($data['parent_id']) ? intval($data['parent_id']) : 0;

    // Get order obj
    $order = resolveProviderDocument($objID, $provider, $type, $parentId);
    if (! $order) {
        return;
    }

    if (is_subclass_of($order, '\WcElectronInvoice\Providers\OrderRefund')) {
        $type  = 'shop_order_refund';
        $order = resolveProviderDocument($order->get_parent_id(), $provider, 'shop_order', $order->get_parent_id());
        if (! $order) {
            return;
        }
    } else {
        $type = 'shop_order';
    }

    $xml_invoice_file              = $order->get_meta('pop_invoice_file_xml') ?: null;
    $xml_credit_note_file          = $order->get_meta('pop_credit_note_file_xml') ?: null;
    $xml_invoice_file_stylized     = $order->get_meta('pop_invoice_file_xml_stylized') ?: null;
    $xml_credit_note_file_stylized = $order->get_meta('pop_credit_note_file_xml_stylized') ?: null;
    $pdf_invoice_file              = $order->get_meta('pop_invoice_file_pdf') ?: null;
    $pdf_credit_note_file          = $order->get_meta('pop_credit_note_file_pdf') ?: null;
    $zugferd_hybrid_invoice_file   = $order->get_meta('pop_invoice_file_zugferd_hybrid_pdf') ?: null;
    $zugferd_hybrid_credit_file    = $order->get_meta('pop_credit_note_file_zugferd_hybrid_pdf') ?: null;

    // If the XML or PDF file already exists, use its content or URL
    if ('xml' === $returnFile &&
        ($xml_invoice_file && file_exists(ABSPATH . $xml_invoice_file) ||
         $xml_credit_note_file && file_exists(ABSPATH . $xml_credit_note_file) ||
         $xml_invoice_file_stylized && file_exists(ABSPATH . $xml_invoice_file_stylized) ||
         $xml_credit_note_file_stylized && file_exists(ABSPATH . $xml_credit_note_file_stylized)
        )
    ) {
        $xml_path = resolveLocalXmlCachePath(
            $type,
            $view,
            $xml_invoice_file,
            $xml_credit_note_file,
            $xml_invoice_file_stylized,
            $xml_credit_note_file_stylized
        );

        if ($xml_path) {
            $full_path = ABSPATH . $xml_path;

            if (! file_exists($full_path)) {
                // I use return otherwise if I want to generate invoices via wp cli and it doesn't find the file it blocks the process
                return;
            }

            if (strpos(realpath($full_path), ABSPATH) !== 0) {
                // Se il percorso non è all'interno della cartella di WordPress, blocca l'accesso
                wp_die('You do not have access to this file', 'Access denied', ['response' => 403]);
            }

            $extension = pathinfo($full_path, PATHINFO_EXTENSION);
            $extension = strtolower($extension);
            if ($extension === 'xml') {
                $mimeType = getMimeType($full_path);
                if ($mimeType !== 'application/xml' && $mimeType !== 'text/xml') {
                    wp_die('File type mismatch', 'Access denied', ['response' => 403]);
                }
            } else {
                wp_die('File type not allowed', 'Access denied', ['response' => 403]);
            }

            if (! isInternalRequest()) {
                wp_die('You do not have access to this file', 'Access denied', ['response' => 403]);
            }

            // single Download file
            if (true === $save && false === $save_bulk) {
                if (file_exists($full_path)) {
                    header('Content-Type: application/octet-stream');
                    header('Content-Disposition: attachment; filename="' . sanitize_file_name(basename($full_path)) . '"');
                    header('Content-Length: ' . filesize($full_path));
                    readfile($full_path);
                    exit;
                }
            } elseif (false === $save_bulk) {
                // Single file redirect for view
                if (file_exists($full_path)) {
                    if (defined('DOING_CRON') && DOING_CRON || defined('DOING_AJAX') && DOING_AJAX) {
                        return file_get_contents($full_path);
                    } else {
                        $mime = getMimeType($full_path);
                        header("Content-Type: {$mime}");
                        header('Content-Disposition: inline; filename="' . sanitize_file_name(basename($full_path)) . '"');
                        readfile($full_path);
                        exit;
                    }
                }
            } else {
                // Return local file string
                return file_get_contents(ABSPATH . $xml_path);
            }
        }
    } elseif ('zugferd_zip' === $returnFile) {
        // Serve a ZIP of all three ZUGFeRD artefacts from local cache.
        // No POP API call here — zero credits consumed when files are already saved.
        $filePaths = ('shop_order_refund' === $type)
            ? [$zugferd_hybrid_credit_file, $pdf_credit_note_file, $xml_credit_note_file]
            : [$zugferd_hybrid_invoice_file, $pdf_invoice_file, $xml_invoice_file];

        $absolutePaths = [];
        foreach ($filePaths as $path) {
            if ($path && file_exists(ABSPATH . $path)) {
                $absolutePaths[] = ABSPATH . $path;
            }
        }

        if (count($absolutePaths) === 3) {
            $tempZipDir = Plugin::getPluginDirPath('/') . 'tempZip/';
            if (! is_dir($tempZipDir)) {
                wp_mkdir_p(trailingslashit($tempZipDir));
            }
            $hybridPath  = 'shop_order_refund' === $type ? $zugferd_hybrid_credit_file : $zugferd_hybrid_invoice_file;
            $bundleBase  = $hybridPath ? preg_replace('/-zugferd\.pdf$/i', '', basename($hybridPath)) : ('zugferd-bundle-' . $objID);
            $zipFileName = sanitize_file_name($bundleBase . '-bundle.zip');
            $zipPath     = $tempZipDir . $zipFileName;
            if (CreateXml::createZip($absolutePaths, $zipPath, true)) {
                header('Content-Type: application/zip');
                header('Content-Disposition: attachment; filename="' . $zipFileName . '"');
                header('Content-Length: ' . filesize($zipPath));
                readfile($zipPath);
                @unlink($zipPath);
                exit;
            }
        }
        // Files not fully cached — fall through so createFromCloud() triggers generation.
        return;
    } elseif (in_array($returnFile, ['pdf', 'zugferd'], true) &&
              (
                  ('pdf' === $returnFile && ($pdf_invoice_file && file_exists(ABSPATH . $pdf_invoice_file) ||
                   $pdf_credit_note_file && file_exists(ABSPATH . $pdf_credit_note_file))) ||
                  ('zugferd' === $returnFile && ($zugferd_hybrid_invoice_file && file_exists(ABSPATH . $zugferd_hybrid_invoice_file) ||
                   $zugferd_hybrid_credit_file && file_exists(ABSPATH . $zugferd_hybrid_credit_file)))
              )
    ) {
        if ('shop_order_refund' === $type) {
            $pdf_path = 'zugferd' === $returnFile ? $zugferd_hybrid_credit_file : $pdf_credit_note_file;
        } else {
            $pdf_path = 'zugferd' === $returnFile ? $zugferd_hybrid_invoice_file : $pdf_invoice_file;
        }
        if ($pdf_path) {
            $full_path = ABSPATH . $pdf_path;

            if (! file_exists($full_path)) {
                // I use return otherwise if I want to generate invoices via wp cli and it doesn't find the file it blocks the process
                return;
            }

            if (strpos(realpath($full_path), ABSPATH) !== 0) {
                // Se il percorso non è all'interno della cartella di WordPress, blocca l'accesso
                wp_die('You do not have access to this file', 'Access denied', ['response' => 403]);
            }

            $extension = pathinfo($full_path, PATHINFO_EXTENSION);
            $extension = strtolower($extension);
            if ($extension === 'pdf') {
                $mimeType = getMimeType($full_path);

                if ($mimeType !== 'application/pdf') {
                    wp_die('File type mismatch', 'Access denied', ['response' => 403]);
                }
            } else {
                wp_die('File type not allowed', 'Access denied', ['response' => 403]);
            }

            if (! isInternalRequest()) {
                wp_die('You do not have access to this file', 'Access denied', ['response' => 403]);
            }

            if (file_exists($full_path)) {
                $mime = getMimeType($full_path); // probabilmente application/pdf
                header("Content-Type: {$mime}");
                $disposition = 'zugferd' === $returnFile ? 'attachment' : 'inline';
                header("Content-Disposition: {$disposition}; filename=\"" . sanitize_file_name(basename($full_path)) . '"');
                readfile($full_path);
                exit;
            }
        }
    }
}

/**
 * Resolve the local cached XML path to serve.
 *
 * For view-mode, stylized XML is preferred when present; otherwise the plain XML
 * file is used as a fallback. This is required for channels such as PEPPOL/KSeF
 * that do not persist dedicated stylized XML files locally.
 *
 * @param string      $documentType
 * @param bool        $view
 * @param string|null $invoiceXmlPath
 * @param string|null $creditNoteXmlPath
 * @param string|null $invoiceStylizedPath
 * @param string|null $creditNoteStylizedPath
 *
 * @return string|null
 */
function resolveLocalXmlCachePath(
    string $documentType,
    bool $view,
    ?string $invoiceXmlPath,
    ?string $creditNoteXmlPath,
    ?string $invoiceStylizedPath,
    ?string $creditNoteStylizedPath
): ?string {
    $isRefund = ('shop_order_refund' === $documentType);

    if (! $view) {
        return $isRefund ? $creditNoteXmlPath : $invoiceXmlPath;
    }

    if ($isRefund) {
        return $creditNoteStylizedPath ?: $creditNoteXmlPath;
    }

    return $invoiceStylizedPath ?: $invoiceXmlPath;
}

/**
 * Resolve which document endpoint actions are available for the current
 * network/customer context.
 *
 * @param string|null $customerType
 * @param string|null $country
 *
 * @return array<string,bool>
 */
function resolveDocumentEndpointActionVisibility(?string $customerType, ?string $country = null): array
{
    if (Integrations::isGermanyZugferdContext()) {
        return [
            'xml'         => false,
            'xml_view'    => false,
            'xml_save'    => false,
            'pdf'         => false,
            'zugferd'     => true,
            'zugferd_zip' => true,
        ];
    }

    $shopCountry  = strtoupper(trim((string)$country));
    $customerType = trim((string)$customerType);
    $context      = Integrations::resolveDocumentEndpointContext($shopCountry);

    if ('' === $shopCountry) {
        $shopCountry = Integrations::getNormalizedShopCountry();
    }

    $pdfOnlyCustomerTypes = apply_filters('wc_el_inv-pdf_only_customer_types_by_native_context', [
        Integrations::ACTIONS_SDI => [
            'SM' => ['private'],
        ],
        Integrations::ACTIONS_PEPPOL => [
            '*' => ['private'],
        ],
        Integrations::ACTIONS_KSEF => [
            '*' => ['private'],
        ],
    ], $shopCountry);

    $isPdfOnly = false;
    if (is_array($pdfOnlyCustomerTypes) && isset($pdfOnlyCustomerTypes[$context]) && is_array($pdfOnlyCustomerTypes[$context])) {
        $contextMap = $pdfOnlyCustomerTypes[$context];

        if (isset($contextMap[$shopCountry]) && is_array($contextMap[$shopCountry])) {
            $isPdfOnly = in_array($customerType, $contextMap[$shopCountry], true);
        } elseif (isset($contextMap['*']) && is_array($contextMap['*'])) {
            $isPdfOnly = in_array($customerType, $contextMap['*'], true);
        }
    }

    return [
        'xml'      => ! $isPdfOnly,
        'xml_view' => ! $isPdfOnly && Integrations::ACTIONS_SDI === $context,
        'xml_save' => ! $isPdfOnly,
        'pdf'      => true,
    ];
}

/**
 * Append standard document endpoint action links.
 *
 * @param string $output
 * @param array  $urls
 * @param bool   $checkSent
 * @param array  $visibility
 *
 * @return string
 */
function appendDocumentEndpointActions(string $output, array $urls, bool $checkSent, array $visibility): string
{
    if (! empty($visibility['zugferd']) && ! empty($urls['zugferd'])) {
        $output .= sprintf(
            '<a class="pop-action button button-primary action-endpoint" target="_blank" href="%1$s" title="%2$s">' .
            '<span class="dashicons dashicons-media-document"></span></a>',
            $urls['zugferd'],
            esc_html__('Download ZUGFeRD PDF', WC_EL_INV_TEXTDOMAIN)
        );
    }

    if (! empty($visibility['zugferd_zip']) && ! empty($urls['zugferd_zip'])) {
        $output .= sprintf(
            '<a class="pop-action button button-secondary action-endpoint" target="_blank" href="%1$s" title="%2$s">' .
            '<span class="dashicons dashicons-media-archive"></span></a>',
            $urls['zugferd_zip'],
            esc_html__('Download ZUGFeRD Bundle (ZIP)', WC_EL_INV_TEXTDOMAIN)
        );
    }

    if (! empty($visibility['xml']) && ! empty($urls['xml'])) {
        $output .= sprintf(
            '<a class="pop-action button button-secondary action-endpoint" %1$s href="%2$s" title="%3$s">' .
            '<span class="dashicons dashicons-editor-ul"></span></a>',
            false === $checkSent ? 'target="_blank"' : '',
            $urls['xml'],
            esc_html__('Get XML', WC_EL_INV_TEXTDOMAIN)
        );
    }

    if (! empty($visibility['xml_view']) && ! empty($urls['xml_view'])) {
        $output .= sprintf(
            '<a class="pop-action button button-primary action-endpoint" %1$s href="%2$s" title="%3$s">' .
            '<span class="dashicons dashicons-visibility"></span></a>',
            false === $checkSent ? 'target="_blank"' : '',
            $urls['xml_view'],
            esc_html__('View XML', WC_EL_INV_TEXTDOMAIN)
        );
    }

    if (! empty($visibility['xml_save']) && ! empty($urls['xml_save'])) {
        $output .= sprintf(
            '<a class="pop-action button button-secondary button-save action-endpoint" %1$s href="%2$s" title="%3$s" %4$s>' .
            '<span class="dashicons dashicons-media-code"></span></a>',
            false === $checkSent ? 'target="_blank"' : '',
            false === $checkSent ? $urls['xml_save'] : 'javascript:;',
            false === $checkSent ?
                esc_html__('Save XML', WC_EL_INV_TEXTDOMAIN) :
                esc_html__('Disabled', WC_EL_INV_TEXTDOMAIN),
            false === $checkSent ? '' : 'disabled="disabled"'
        );
    }

    if (! empty($visibility['pdf']) && ! empty($urls['pdf'])) {
        $output .= sprintf(
            '<a class="pop-action button button-secondary button-pdf action-endpoint" %1$s href="%2$s" title="%3$s">' .
            '<span class="dashicons dashicons-media-text"></span></a>',
            'target="_blank"',
            $urls['pdf'],
            esc_html__('View PDF', WC_EL_INV_TEXTDOMAIN)
        );
    }

    return $output;
}

/**
 * Return absolute existing cached document paths for the given order.
 *
 * @param mixed $order
 *
 * @return array<string,string>
 */
function getCachedDocumentAttachmentPaths($order): array
{
    $paths = getCachedDocumentAttachmentMetaPaths($order);
    $resolved = [];
    foreach ($paths as $key => $relativePath) {
        if ('' === $relativePath) {
            continue;
        }

        $absolutePath = ABSPATH . ltrim($relativePath, '/');
        if (file_exists($absolutePath) && is_file($absolutePath)) {
            $resolved[$key] = $absolutePath;
        }
    }

    return $resolved;
}

/**
 * Return raw cached document meta paths without checking file existence.
 *
 * @param mixed $order
 *
 * @return array<string,string>
 */
function getCachedDocumentAttachmentMetaPaths($order): array
{
    if (! is_object($order) || ! method_exists($order, 'get_meta')) {
        return [];
    }

    $isRefund = is_subclass_of($order, '\WcElectronInvoice\Providers\OrderRefund');
    $keys = $isRefund ? [
        'xml' => 'pop_credit_note_file_xml',
        'pdf' => 'pop_credit_note_file_pdf',
        'zugferd_hybrid_pdf' => 'pop_credit_note_file_zugferd_hybrid_pdf',
    ] : [
        'xml' => 'pop_invoice_file_xml',
        'pdf' => 'pop_invoice_file_pdf',
        'zugferd_hybrid_pdf' => 'pop_invoice_file_zugferd_hybrid_pdf',
    ];

    // Force each provider to reload meta from its data store before reading.
    // WooCommerce Order re-reads from HPOS; other providers implement a no-op.
    $order->refresh_meta_data();

    $paths = [];
    foreach ($keys as $alias => $metaKey) {
        $paths[$alias] = (string)$order->get_meta($metaKey);
    }

    return $paths;
}

/**
 * Return cached document attachment entries with labels and file status.
 *
 * @param mixed $order
 * @param bool  $includeRelated When true, include both invoice and credit note slots.
 *
 * @return array<int,array<string,mixed>>
 */
function getCachedDocumentAttachmentDisplayEntries($order, bool $includeRelated = false): array
{
    if (! is_object($order) || ! method_exists($order, 'get_meta')) {
        return [];
    }

    $definitions = getCachedDocumentAttachmentDefinitions($order, $includeRelated);
    if (empty($definitions)) {
        return [];
    }

    if (method_exists($order, 'refresh_meta_data')) {
        $order->refresh_meta_data();
    }

    $entries = [];
    foreach ($definitions as $metaKey => $definition) {
        $relativePath = (string)$order->get_meta($metaKey);
        if ('' === $relativePath) {
            continue;
        }

        $absolutePath = ABSPATH . ltrim($relativePath, '/');
        $exists       = file_exists($absolutePath) && is_file($absolutePath);

        $entries[] = [
            'meta_key'        => $metaKey,
            'document_type'   => $definition['document_type'],
            'document_label'  => $definition['document_label'],
            'format'          => $definition['format'],
            'format_label'    => $definition['format_label'],
            'relative_path'   => $relativePath,
            'absolute_path'   => $absolutePath,
            'exists'          => $exists,
        ];
    }

    return $entries;
}

/**
 * Delete cached document attachments and related meta keys.
 *
 * @param mixed $order
 * @param bool  $includeRelated When true, include both invoice and credit note slots.
 *
 * @return array<string,mixed>
 */
function clearCachedDocumentAttachmentCache($order, bool $includeRelated = false): array
{
    $entries = getCachedDocumentAttachmentDisplayEntries($order, $includeRelated);
    $result  = [
        'deleted_files'     => 0,
        'deleted_meta'      => 0,
        'deleted_meta_keys' => [],
    ];

    if (empty($entries) || ! method_exists($order, 'delete_meta')) {
        return $result;
    }

    foreach ($entries as $entry) {
        if (! empty($entry['absolute_path']) && file_exists($entry['absolute_path']) && is_file($entry['absolute_path'])) {
            if (@unlink($entry['absolute_path'])) {
                $result['deleted_files']++;
            }
        }

        $order->delete_meta($entry['meta_key']);
        $result['deleted_meta']++;
        $result['deleted_meta_keys'][] = $entry['meta_key'];
    }

    if ($result['deleted_meta'] > 0 && method_exists($order, 'save')) {
        $order->save();
    }

    return $result;
}

/**
 * Resolve the cached attachment meta definitions for the provided document.
 *
 * @param mixed $order
 * @param bool  $includeRelated
 *
 * @return array<string,array<string,string>>
 */
function getCachedDocumentAttachmentDefinitions($order, bool $includeRelated = false): array
{
    $allDefinitions = [
        'pop_invoice_file_xml' => [
            'document_type'  => 'invoice',
            'document_label' => __('Invoice', WC_EL_INV_TEXTDOMAIN),
            'format'         => 'xml',
            'format_label'   => __('XML', WC_EL_INV_TEXTDOMAIN),
        ],
        'pop_invoice_file_xml_stylized' => [
            'document_type'  => 'invoice',
            'document_label' => __('Invoice', WC_EL_INV_TEXTDOMAIN),
            'format'         => 'xml_stylized',
            'format_label'   => __('Stylized XML', WC_EL_INV_TEXTDOMAIN),
        ],
        'pop_invoice_file_pdf' => [
            'document_type'  => 'invoice',
            'document_label' => __('Invoice', WC_EL_INV_TEXTDOMAIN),
            'format'         => 'pdf',
            'format_label'   => __('PDF', WC_EL_INV_TEXTDOMAIN),
        ],
        'pop_invoice_file_zugferd_hybrid_pdf' => [
            'document_type'  => 'invoice',
            'document_label' => __('Invoice', WC_EL_INV_TEXTDOMAIN),
            'format'         => 'zugferd_hybrid_pdf',
            'format_label'   => __('ZUGFeRD PDF', WC_EL_INV_TEXTDOMAIN),
        ],
        'pop_credit_note_file_xml' => [
            'document_type'  => 'credit_note',
            'document_label' => __('Credit Note', WC_EL_INV_TEXTDOMAIN),
            'format'         => 'xml',
            'format_label'   => __('XML', WC_EL_INV_TEXTDOMAIN),
        ],
        'pop_credit_note_file_xml_stylized' => [
            'document_type'  => 'credit_note',
            'document_label' => __('Credit Note', WC_EL_INV_TEXTDOMAIN),
            'format'         => 'xml_stylized',
            'format_label'   => __('Stylized XML', WC_EL_INV_TEXTDOMAIN),
        ],
        'pop_credit_note_file_pdf' => [
            'document_type'  => 'credit_note',
            'document_label' => __('Credit Note', WC_EL_INV_TEXTDOMAIN),
            'format'         => 'pdf',
            'format_label'   => __('PDF', WC_EL_INV_TEXTDOMAIN),
        ],
        'pop_credit_note_file_zugferd_hybrid_pdf' => [
            'document_type'  => 'credit_note',
            'document_label' => __('Credit Note', WC_EL_INV_TEXTDOMAIN),
            'format'         => 'zugferd_hybrid_pdf',
            'format_label'   => __('ZUGFeRD PDF', WC_EL_INV_TEXTDOMAIN),
        ],
    ];

    if ($includeRelated) {
        return $allDefinitions;
    }

    $isRefund = is_subclass_of($order, '\WcElectronInvoice\Providers\OrderRefund');
    if (! $isRefund && is_object($order) && method_exists($order, 'get_type')) {
        $isRefund = 'shop_order_refund' === (string)$order->get_type();
    }

    $targetDocumentType = $isRefund ? 'credit_note' : 'invoice';

    return array_filter($allDefinitions, static function ($definition) use ($targetDocumentType) {
        return $definition['document_type'] === $targetDocumentType;
    });
}

/**
 * Resolve completed-order attachments for Germany ZUGFeRD.
 *
 * It reuses cached files when available and triggers at most one bundle
 * generation request when the cache is incomplete.
 *
 * @param mixed $order
 *
 * @return array<int,string>
 */
function getZugferdCompletedOrderAttachments($order): array
{
    if (! Integrations::isGermanyZugferdContext() || ! is_object($order) || ! method_exists($order, 'get_id')) {
        return [];
    }

    $providerOrder = OrderQuery::instance()->getProviderOrder($order->get_id(), 'woocommerce');
    if (! $providerOrder || ! is_subclass_of($providerOrder, '\WcElectronInvoice\Providers\Order')) {
        return [];
    }

    $cachedPaths = getCachedDocumentAttachmentPaths($providerOrder);
    if (isset($cachedPaths['xml'], $cachedPaths['pdf'], $cachedPaths['zugferd_hybrid_pdf'])) {
        return array_values($cachedPaths);
    }

    $key = OptionPage::getK();
    $url = esc_url_raw(add_query_arg([
        'shop_order'  => $providerOrder->get_id(),
        'provider'    => 'woocommerce',
        'key'         => $key,
        'format'      => 'zugferd',
        'choice_type' => 'invoice',
        'v'           => time(),
    ], site_url('/' . \WcElectronInvoice\EndPoint\Endpoints::ENDPOINT)));

    wp_remote_get($url, getSessionCookieArgs('application/pdf'));

    $providerOrder = OrderQuery::instance()->getProviderOrder($order->get_id(), 'woocommerce');
    if (! $providerOrder || ! is_subclass_of($providerOrder, '\WcElectronInvoice\Providers\Order')) {
        return [];
    }

    $cachedPaths = getCachedDocumentAttachmentPaths($providerOrder);
    if (isset($cachedPaths['xml'], $cachedPaths['pdf'], $cachedPaths['zugferd_hybrid_pdf'])) {
        return array_values($cachedPaths);
    }

    return [];
}

/**
 * xmlNoInCache
 *
 * @param     $order
 * @param int $counter
 *
 * @return int
 */
function xmlNoInCache($order, int $counter = 0): int
{
    $invoice_file     = $order->get_meta('pop_invoice_file_xml');
    $credit_note_file = $order->get_meta('pop_credit_note_file_xml');

    $invoice_exists     = $invoice_file && file_exists(ABSPATH . $invoice_file);
    $credit_note_exists = $credit_note_file && file_exists(ABSPATH . $credit_note_file);

    if (! $invoice_exists && ! $credit_note_exists) {
        $counter++;
    }

    return $counter;
}

/**
 * getMimeType
 *
 * @param $full_path
 *
 * @return string
 */
function getMimeType($full_path): string
{
    $extension = strtolower(pathinfo($full_path, PATHINFO_EXTENSION));

    $mimeTypes = [
        'xml' => 'application/xml',
        'pdf' => 'application/pdf',
        'csv' => 'text/csv',
        'txt' => 'text/plain',
        'zip' => 'application/zip',
    ];

    if (isset($mimeTypes[$extension])) {
        return $mimeTypes[$extension];
    }

    if (function_exists('mime_content_type')) {
        return mime_content_type($full_path);
    }

    return 'application/octet-stream';
}

/**
 * isInternalRequest
 *
 * @return bool
 */
function isInternalRequest()
{
    return is_user_logged_in() || defined('DOING_CRON') || defined('DOING_AJAX') || defined('REST_REQUEST');
}

/**
 * popNoticeIfNginx
 *
 * @return void
 */
function popNoticeIfNginx()
{
    $is_wpe = function_exists('is_wpe') && is_wpe();

    if (isset($GLOBALS['is_nginx'])) {
        $is_nginx = (bool)$GLOBALS['is_nginx'];
    } else {
        $is_nginx = false;
        if (isset($_SERVER['SERVER_SOFTWARE'])) {
            $is_nginx = stripos($_SERVER['SERVER_SOFTWARE'], 'nginx') !== false;
        }
    }

    if ($is_wpe) {
        $is_nginx = true;
    }

    $is_nginx = apply_filters('pop_is_nginx_server', $is_nginx);

    if ($is_nginx) {
        $page = \WcElectronInvoice\Functions\filterInput($_GET, 'page');
        $tab  = \WcElectronInvoice\Functions\filterInput($_GET, 'tab', FILTER_UNSAFE_RAW);

        if ('wc_el_inv-options-page' === $page && 'invoice' === $tab) {
            echo '<div class="pop-notice notice-info">';
            echo sprintf(
                '<p><span class="dashicons dashicons-info"></span> <strong>%s</strong> %s <a href="%s" target="_blank">%s</a></p>',
                esc_html__('You are using NGINX:', WC_EL_INV_TEXTDOMAIN),
                esc_html__('You can apply custom rewrite or protection rules directly in the server configuration file.',
                    WC_EL_INV_TEXTDOMAIN),
                esc_url('https://docs.popapi.io/it/guides/configuration/generated-files-storage/'),
                esc_html__('Read the guide', WC_EL_INV_TEXTDOMAIN)
            );
            echo '</div>';
        }
    }
}

/**
 * sanMarinoSupport
 *
 * @param $customerType
 * @param $customerCountry
 *
 * @return object
 */
function sanMarinoSupport($customerType = null, $customerCountry = null): object
{
    $noViesCheck   = false;
    $shopSMCountry = get_option('wc_el_inv-general_store_sm_country', null);
    if ('yes' === $shopSMCountry || defined('WC_EL_INV_SM_TEST') && WC_EL_INV_SM_TEST) {
        $shopSMCountry = 'SM';
    }

    if (! $customerType && ! $customerCountry) {
        return (object)[
            'shopCountry'     => $shopSMCountry,
            'customerType'    => null,
            'customerCountry' => null,
            'nature'          => null,
            'refNormative'    => null,
            'noViesCheck'     => null,
        ];
    }

    if ('SM' === $shopSMCountry) {
        if ('private' === $customerType && 'IT' === $customerCountry) {
            $nature            = "N2.2";
            $refNormativeValue = "Fuori campo IVA";
            $noViesCheck       = true;
        } elseif (('company' === $customerType || 'freelance' === $customerType) && 'IT' === $customerCountry) {
            $nature            = "N3.3";
            $refNormativeValue = "Non imp. art. 71 D.P.R. 633/72";
            $noViesCheck       = true;
        }
    }

    return (object)[
        'shopCountry'     => $shopSMCountry,
        'customerType'    => $customerType,
        'customerCountry' => $customerCountry,
        'nature'          => $nature ?? '',
        'refNormative'    => $refNormativeValue ?? '',
        'noViesCheck'     => $noViesCheck,
    ];
}

/**
 * popReviewNoticeInit
 *
 * @return void
 */
function popReviewNoticeInit()
{
    $uidOption = (int)get_option('pop_account_user_id', false);
    if (! $uidOption) {
        return;
    }

    $userID         = get_current_user_id();
    $shouldRedirect = false;

    // Set activation date if none exists
    if (! get_option('pop_review_activation_date')) {
        update_option('pop_review_activation_date', current_time('timestamp'));
    }

    // If the user clicked "Do not show again"
    if (isset($_GET['pop_hide_review_notice_forever']) && current_user_can(apply_filters('wc_el_inv-options-capability', Plugin::$capability))) {
        update_user_meta($userID, 'pop_hide_review_notice_forever', 1);
        $shouldRedirect = true;
    }

    // If the user has temporarily dismissed the notice
    if (isset($_GET['pop_dismiss_review_notice']) && current_user_can(apply_filters('wc_el_inv-options-capability', Plugin::$capability))) {
        update_user_meta($userID, 'pop_review_notice_dismissed_at', current_time('timestamp'));
        $shouldRedirect = true;
    }

    if ($shouldRedirect) {
        // Rebuilds the current URL without custom parameters
        $url = remove_query_arg(['pop_dismiss_review_notice', 'pop_hide_review_notice_forever']);
        wp_safe_redirect($url);
        exit;
    }
}

/**
 * popReviewNoticeDisplay
 *
 * @return void
 */
function popReviewNoticeDisplay()
{
    $page   = filterInput($_GET, 'page', FILTER_SANITIZE_SPECIAL_CHARS) ?: null;
    $isProd = (defined('WC_EL_INV_ENV') && WC_EL_INV_ENV === 'prod');

    $uidOption = (int)get_option('pop_account_user_id', false);
    if (! $uidOption) {
        return;
    }

    $userID = get_current_user_id();

    if (! isset($page) || $page !== 'wc_el_inv-options-page') {
        return;
    }

    if (! current_user_can(apply_filters('wc_el_inv-options-capability', Plugin::$capability)) && 'prod' == WC_EL_INV_ENV) {
        return;
    }

    $dismissedForever = (bool)get_user_meta($userID, 'pop_hide_review_notice_forever', true);
    if ($dismissedForever && 'prod' == WC_EL_INV_ENV) {
        return;
    }

    $activationTime = get_option('pop_review_activation_date');
    if (
        $isProd &&
        (
            ! $activationTime ||
            (time() - $activationTime) < 14 * DAY_IN_SECONDS
        )
    ) {
        return;
    }

    $dismissedAt = (int)get_user_meta($userID, 'pop_review_notice_dismissed_at', true);
    if (
        $isProd &&
        $dismissedAt &&
        (time() - $dismissedAt) < 30 * DAY_IN_SECONDS
    ) {
        return;
    }

    $dismissUrl     = add_query_arg('pop_dismiss_review_notice', '1');
    $hideForeverUrl = add_query_arg('pop_hide_review_notice_forever', '1');
    $licenseKey     = OptionPage::init()->getOptions('license_key') ?? null;

    ?>
    <div class="notice notice-info pop_reviews is-dismissible">
        <p>
            <strong>
                <?php echo _x('Let others know what you think!', 'review notice', WC_EL_INV_TEXTDOMAIN); ?>
            </strong><br>
            <?php
            printf(
                '%s<br> <a href="%s" target="_blank" rel="noopener noreferrer">%s <span class="stars">
<span class="dashicons dashicons-star-filled"></span>
<span class="dashicons dashicons-star-filled"></span>
<span class="dashicons dashicons-star-filled"></span>
<span class="dashicons dashicons-star-filled"></span>
<span class="dashicons dashicons-star-filled"></span></span></a><br><small style="cursor: help; color: #666;"><i>%s</i></small>',
                _x('Your review helps improve POP and guide others in choosing the right plugin.', 'review notice',
                    WC_EL_INV_TEXTDOMAIN),
                WC_EL_INV_API_DOMAIN . "?reviews=add&token={$licenseKey}",
                _x('Leave a 5-star review', 'review notice', WC_EL_INV_TEXTDOMAIN),
                _x('Personalized link, used for statistical purposes only.', 'review notice', WC_EL_INV_TEXTDOMAIN),
            );
            ?>
        </p>
        <p>
            <a href="<?php echo esc_url($dismissUrl); ?>" class="button-primary">
                <?php echo _x('Remind me later', 'review notice', WC_EL_INV_TEXTDOMAIN); ?>
            </a>
            <a href="<?php echo esc_url($hideForeverUrl); ?>" class="button button-secondary">
                <?php echo _x('Don’t show again', 'review notice', WC_EL_INV_TEXTDOMAIN); ?>
            </a>
        </p>
    </div>

    <?php
}

/**
 * @return void
 */
function processFileBatch()
{
    $offset = filterInput($_POST, 'offset', FILTER_VALIDATE_INT);
    $limit  = filterInput($_POST, 'limit', FILTER_VALIDATE_INT);

    if (! check_ajax_referer('wc_el_inv_ajax-ajax_nonce', 'nonce', false)) {
        wp_send_json_error(['message' => esc_html__('Unauthorized.', WC_EL_INV_TEXTDOMAIN)]);
        return;
    }

    if (! isUserPlanLevelGte(getCurrentUserPlanLevel(), 'growth')) {
        wp_send_json_error([
            'message' => esc_html__('Bulk download is available starting from the Growth plan.', WC_EL_INV_TEXTDOMAIN),
        ]);
    }

    $storedTempData = get_option('temp_order_filtered');
    if (! $storedTempData) {
        wp_send_json_error(['message' => 'Nessun dato trovato.']);
    }

    if (! is_user_logged_in() &&
        ! current_user_can(apply_filters('wc_el_inv-options-capability', Plugin::$capability)) &&
        ! current_user_can('manage_network')
    ) {
        wp_send_json_error(['message' => 'Non autorizzato']);
    }

    if ($offset === 0) {
        delete_transient('pop_xml_files_created');
        CreateXml::deleteTempXmlFiles(true);
        CreateXml::deleteTempZipFiles(true);
    }

    $batch          = array_slice($storedTempData, $offset, $limit);
    $generatedFiles = [];
    foreach ($batch as $xml) {
        $provider = '';
        $documentType = 'shop_order';
        $parentId = 0;
        if (is_object($xml)) {
            $provider = $xml->provider ?? $xml->order_provider ?? '';
            $documentType = $xml->document_type ?? $xml->order_type ?? 'shop_order';
            $parentId = isset($xml->parent_id) ? (int)$xml->parent_id : 0;
        } elseif (is_array($xml)) {
            $provider = $xml['provider'] ?? $xml['order_provider'] ?? '';
            $documentType = $xml['document_type'] ?? $xml['order_type'] ?? 'shop_order';
            $parentId = isset($xml['parent_id']) ? (int)$xml['parent_id'] : 0;
        }
        $provider = $provider ?: 'woocommerce';
        $documentType = sanitize_key((string)$documentType);

        $order = resolveProviderDocument($xml->id, $provider, $documentType, $parentId);

        if (! $order) {
            continue;
        }

        if (is_subclass_of($order, '\WcElectronInvoice\Providers\OrderRefund')) {
            $parentOrder = resolveProviderDocument($order->get_parent_id(), $provider, 'shop_order', $order->get_parent_id());
            if (! $parentOrder) {
                continue;
            }

            $invoice_file     = '';
            $credit_note_file = $parentOrder->get_meta('pop_credit_note_file_xml');
        } else {
            $invoice_file     = $order->get_meta('pop_invoice_file_xml');
            $credit_note_file = $order->get_meta('pop_credit_note_file_xml');
        }

        $invoice_path     = ABSPATH . $invoice_file;
        $credit_note_path = ABSPATH . $credit_note_file;

        $invoice_exists     = $invoice_file && file_exists($invoice_path);
        $credit_note_exists = $credit_note_file && file_exists($credit_note_path);

        log(sprintf(
            '[processFileBatch] order #%s type=%s | invoice_file=%s | invoice_exists=%s | credit_note_exists=%s',
            $xml->id ?? '?',
            $documentType,
            $invoice_file ?: '(none)',
            $invoice_exists ? 'YES' : 'NO',
            $credit_note_exists ? 'YES' : 'NO'
        ));

        if ('shop_order_refund' === $documentType && $credit_note_exists) {
            log('[processFileBatch] → using cached credit note: ' . $credit_note_path);
            $generatedFiles[] = $credit_note_path;
        } elseif ('shop_order_refund' !== $documentType && $invoice_exists) {
            log('[processFileBatch] → using cached invoice: ' . $invoice_path);
            $generatedFiles[] = $invoice_path;
        } elseif ($credit_note_exists) {
            log('[processFileBatch] → using cached credit note fallback: ' . $credit_note_path);
            $generatedFiles[] = $credit_note_path;
        } else {
            log('[processFileBatch] → no cache, generating via singleOrderXmlData()');
            // Lightweight summaries in temp_order_filtered carry only list-view fields (id, total, dates, etc.).
            // createFromCloud() needs the full order data (billing, items, vat_number, sdi_type, …).
            // Re-build via BuildXml::singleOrderXmlData(), which is the same path used for single-order XML.
            $orderProviderInterface = 'shop_order_refund' === $documentType
                ? '\WcElectronInvoice\Providers\OrderRefund'
                : '\WcElectronInvoice\Providers\Order';

            $builder = new \WcElectronInvoice\Xml\BuildXml();
            $builder->singleOrderXmlData($order, $orderProviderInterface);
            $xmlLoop = $builder->getXmlData();

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

            $data             = new CreateXml(new \SimpleXMLElement(
                '<?xml version="1.0" encoding="UTF-8"?><xmlns:p:FatturaElettronica/>',
                LIBXML_NOERROR,
                false,
                'p',
                true
            ));
            $generatedFiles[] = $data->create($xmlLoop);
        }
    }

    $existingFiles = get_transient('pop_xml_files_created') ?: [];
    $allFiles      = array_merge($existingFiles, $generatedFiles);
    set_transient('pop_xml_files_created', $allFiles, 60 * 30); // 30 minuti

    $done = ($offset + $limit) >= count($storedTempData);

    if ($done) {
        // File name
        $vatNumber   = preg_replace('/^' . preg_quote(GeneralFields::getGeneralInvoiceOptionCountryState(),
                '/') . '/',
            '', GeneralFields::getGeneralInvoiceOptionVatNumber()
        );
        $fileName    = GeneralFields::getGeneralInvoiceOptionCountryState() . $vatNumber;
        $zipFileName = "{$fileName}_" . date('Y-m-d_h-i');

        $tempZipDir = Plugin::getPluginDirPath('/') . "/tempZip/";
        if (! is_dir($tempZipDir)) {
            wp_mkdir_p(trailingslashit($tempZipDir));
            if (! is_writable($tempZipDir)) {
                @chmod($tempZipDir, 0755);
                if (! is_writable($tempZipDir)) {
                    wp_send_json_error(esc_html__('The directory is not writable even after attempting to change permissions. Please check server settings.',
                            WC_EL_INV_TEXTDOMAIN) . ' DIR: ' . $tempZipDir);
                }
            }
        }

        $zipCreated = CreateXml::createZip($allFiles,
            Plugin::getPluginDirPath('/') . "/tempZip/{$zipFileName}.zip");

        if ($zipCreated) {
            delete_transient('xml_batch_data');
            delete_transient('xml_files_created');

            wp_send_json_success([
                'done'      => true,
                'current'   => $offset + $limit,
                'all_files' => $allFiles,
                'zip_url'   => Plugin::getPluginDirUrl('/') . "/tempZip/{$zipFileName}.zip",
            ]);
        } else {
            wp_send_json_error(['message' => 'Errore: Zip non creato']);
        }
    } else {
        wp_send_json_success([
            'done'          => false,
            'current'       => $offset + $limit,
            'current_files' => $allFiles,
            'total'         => count($storedTempData), // aggiungi questo
        ]);
    }
}

/**
 * isWcBlockCheckoutPage
 *
 * @return bool
 */
function isWcBlockCheckoutPage()
{
    if (! function_exists('has_block')) {
        return false;
    }

    global $post;

    if (! $post instanceof \WP_Post) {
        return false;
    }

    return has_block('woocommerce/checkout', $post->ID);
}

/**
 * isStoreApiRequest
 *
 * @return bool
 */
function isStoreApiRequest()
{
    static $result;
    if (null !== $result) {
        return $result;
    }

    $result = false;

    if (defined('REST_REQUEST') && REST_REQUEST) {
        if (isset($GLOBALS['wp']->query_vars['rest_route'])) {
            $rest_route = (string)$GLOBALS['wp']->query_vars['rest_route'];
            if (false !== strpos($rest_route, 'wc/store')) {
                $result = true;
            }
        }

        if (! $result && ! empty($_SERVER['REQUEST_URI'])) {
            $uri = wp_unslash($_SERVER['REQUEST_URI']);
            if (false !== strpos($uri, '/wc/store/') || false !== strpos($uri, '/wc/store')) {
                $result = true;
            }
        }
    }

    return (bool)apply_filters('pop_is_store_api_request', $result);
}

/**
 * pmProViesCheck
 *
 * @return void
 */
function popCheckoutViesCheck()
{
    $customerInvoiceType = trim(sanitize(filterInput($_POST, 'invoice_type'))) ?: null;
    $customerChoiceType  = trim(sanitize(filterInput($_POST, 'choice_type'))) ?: null;
    $taxCode             = trim(sanitize(filterInput($_POST, 'tax_code'))) ?: null;
    $vatNumber           = trim(sanitize(filterInput($_POST, 'vat_number'))) ?: null;
    $country             = trim(sanitize(filterInput($_POST, 'country'))) ?: null;

    if ('yes' !== \WcElectronInvoice\WooCommerce\Fields\TaxFields::getViesCheck()) {
        if (function_exists('WC') && WC()->customer) {
            WC()->customer->set_is_vat_exempt(false);
        }

        wp_send_json_success([
            'message'       => null,
            'vies'          => 'not_necessary',
            'log_message'   => esc_html__('Vies control not necessary', WC_EL_INV_TEXTDOMAIN),
            'update_totals' => true,
        ]);
    }

    // Vies validation only occurs when the customer's country is
    // different from the seller's country e.g. (FR customer, IT seller)
    if (\WcElectronInvoice\WooCommerce\Fields\GeneralFields::getGeneralInvoiceOptionCountryState() === $country) {
        wp_send_json_success([
            'message'       => null,
            'vies'          => 'not_necessary',
            'log_message'   => esc_html__('Vies control not necessary', WC_EL_INV_TEXTDOMAIN),
            'update_totals' => true,
        ]);
    }

    if ('receipt' === $customerChoiceType) {
        wp_send_json_success([
            'message'       => null,
            'vies'          => 'not_necessary',
            'log_message'   => esc_html__('Vies control not necessary', WC_EL_INV_TEXTDOMAIN),
            'update_totals' => true,
        ]);
    }

    if ('private' !== $customerInvoiceType && empty($vatNumber) || empty($country)) {
        wp_send_json_error([
            'message'       => esc_html__('Missing data for VAT verification.', WC_EL_INV_TEXTDOMAIN),
            'vies'          => 'error_missing_data',
            'update_totals' => true,
        ]);
    }

    $vatCode = normalizeVatForVies($vatNumber, $country);

    // Retry count
    $retryCacheKey = 'wc_el_inv-vies_' . $country . '_' . $vatCode . '_retries';
    $retryCount    = (int)get_transient($retryCacheKey);

    try {
        $valid = InvoiceFields::viesCheck($vatCode, $country);

        if ($valid) {
            WC()->customer->set_is_vat_exempt(true);
            wp_send_json_success([
                'message'       => esc_html__('VAT valid for VIES', WC_EL_INV_TEXTDOMAIN),
                'vies'          => $valid,
                'country'       => $country,
                'vat_code'      => $vatCode,
                'retry_count'   => $retryCount,
                'update_totals' => false,
                // it is not necessary as the removal of taxes already causes a change in the data
            ]);
        } else {
            WC()->customer->set_is_vat_exempt(false);
            wp_send_json_error([
                'message'       => esc_html__('The VAT number entered is not valid for VIES', WC_EL_INV_TEXTDOMAIN),
                'vies'          => $valid,
                'country'       => $country,
                'vat_code'      => $vatCode,
                'retry_count'   => $retryCount,
                'update_totals' => true,
            ]);
        }
    } catch (\Exception $e) {
        wp_send_json_error([
            'message'       => esc_html__('Error during VIES verification: ', WC_EL_INV_TEXTDOMAIN) . $e->getMessage(),
            'vies'          => 'error_data',
            'country'       => $country,
            'vat_code'      => $vatCode,
            'retry_count'   => $retryCount,
            'update_totals' => true,
        ]);
    }
}

/**
 * Normalize VIES member state code when VIES expects a non-ISO prefix.
 *
 * @param string|null $country
 *
 * @return string
 */
function normalizeViesCountryCode($country = null)
{
    $countryCode = strtoupper((string)$country);
    $countryCode = preg_replace('/[^A-Z]/', '', $countryCode);

    if ('GR' === $countryCode) {
        return 'EL';
    }

    return (string)$countryCode;
}

/**
 * Normalize VAT number for VIES checks (IT + EU).
 *
 * Keeps only alphanumeric characters, uppercases letters,
 * and removes an optional leading country prefix when present.
 *
 * @param string|null $vatNumber
 * @param string|null $country
 *
 * @return string
 */
function normalizeVatForVies($vatNumber, $country = null)
{
    $vatCode = strtoupper((string)$vatNumber);
    $vatCode = preg_replace('/[^A-Z0-9]/', '', $vatCode);

    $countryCode = normalizeViesCountryCode($country);

    if (! empty($countryCode) && 0 === strpos($vatCode, $countryCode)) {
        $vatCode = substr($vatCode, strlen($countryCode));
    }

    return (string)$vatCode;
}
