<?php
/**
 * ZERTI plugin — functions and hooks.
 *
 * Loaded from zerti.php. Expects constants ZERTI_* and ZERTI_PLUGIN_FILE.
 */

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

/**
 * Unslash the current POST payload for sanitizing helpers.
 *
 * @return array<string, mixed>
 */
function zerti_unslash_post_array(): array
{
    // phpcs:disable WordPress.Security.NonceVerification.Missing -- Unslash helper only; callers verify nonce before reading fields.
    if (!isset($_POST) || !is_array($_POST)) {
        return [];
    }

    return wp_unslash($_POST);
    // phpcs:enable WordPress.Security.NonceVerification.Missing
}

/**
 * Read a file from the plugin directory using WP_Filesystem (path must stay under plugin root).
 */
function zerti_read_plugin_file(string $relative_path): string
{
    $relative_path = ltrim(str_replace('\\', '/', $relative_path), '/');
    if ($relative_path === '' || strpos($relative_path, '..') !== false) {
        return '';
    }

    $plugin_root = plugin_dir_path(ZERTI_PLUGIN_FILE);
    $absolute = $plugin_root . str_replace('/', DIRECTORY_SEPARATOR, $relative_path);
    $real_root = realpath($plugin_root);
    $real_path = realpath($absolute);

    if ($real_root === false || $real_path === false || !is_readable($real_path)) {
        return '';
    }

    $real_root = rtrim(str_replace('\\', '/', $real_root), '/') . '/';
    $real_path_norm = str_replace('\\', '/', $real_path);
    if (strpos($real_path_norm, $real_root) !== 0) {
        return '';
    }

    global $wp_filesystem;
    if (empty($wp_filesystem)) {
        require_once ABSPATH . 'wp-admin/includes/file.php';
        WP_Filesystem();
    }

    if (empty($wp_filesystem) || !is_object($wp_filesystem)) {
        return '';
    }

    $contents = $wp_filesystem->get_contents($real_path);

    return is_string($contents) ? $contents : '';
}

/**
 * Capture echoed output from a callback into a string.
 */
function zerti_capture_output(callable $callback): string
{
    ob_start();
    $callback();
    $html = ob_get_clean();

    return is_string($html) ? $html : '';
}

/**
 * URL to the ZERTI logo (in-app sidebar / branding).
 */
function zerti_logo_url(): string
{
    $relative = 'assets/images/zerti-logo-light.png';
    $path = plugin_dir_path(ZERTI_PLUGIN_FILE) . $relative;
    $version = is_readable($path) ? (string) filemtime($path) : ZERTI_VERSION;

    return add_query_arg('ver', $version, plugins_url($relative, ZERTI_PLUGIN_FILE));
}

/**
 * URL to the ZERTI icon for the WordPress admin left menu only.
 */
function zerti_admin_menu_logo_url(): string
{
    $relative = 'assets/images/zerti-admin-menu-icon.png';
    $path = plugin_dir_path(ZERTI_PLUGIN_FILE) . $relative;
    $version = is_readable($path) ? (string) filemtime($path) : ZERTI_VERSION;

    return add_query_arg('ver', $version, plugins_url($relative, ZERTI_PLUGIN_FILE));
}

/**
 * Public site / store display name (WordPress site title).
 */
function zerti_site_display_name(): string
{
    $name = function_exists('get_bloginfo') ? trim((string) get_bloginfo('name')) : '';
    if ($name === '' && function_exists('get_option')) {
        $name = trim((string) get_option('blogname', ''));
    }

    /**
     * Filters the store name shown on order detail / invoice documents.
     *
     * @param string $name Site title.
     */
    return (string) apply_filters('zerti_site_display_name', $name);
}

/**
 * Website logo URL for invoices (custom logo, then site icon).
 */
function zerti_site_logo_url(): string
{
    $logo_id = (int) get_theme_mod('custom_logo');
    if ($logo_id > 0) {
        $url = wp_get_attachment_image_url($logo_id, 'medium');
        if (is_string($url) && $url !== '') {
            return (string) apply_filters('zerti_site_logo_url', $url);
        }
    }

    $icon_id = (int) get_option('site_icon');
    if ($icon_id > 0) {
        $url = wp_get_attachment_image_url($icon_id, 'medium');
        if (is_string($url) && $url !== '') {
            return (string) apply_filters('zerti_site_logo_url', $url);
        }
    }

    return (string) apply_filters('zerti_site_logo_url', '');
}

/**
 * WooCommerce store address as a single plain-text line (Persian-friendly separators).
 */
function zerti_wc_get_store_address_plain(): string
{
    if (!class_exists('WooCommerce') || !function_exists('WC')) {
        return '';
    }
    $wc = WC();
    if (!$wc || !$wc->countries) {
        return '';
    }

    $default = (string) get_option('woocommerce_default_country', '');
    $country = $default;
    $state = '';
    if (strpos($default, ':') !== false) {
        $split = explode(':', $default, 2);
        $country = $split[0];
        $state = $split[1] ?? '';
    }

    $address = [
        'address_1' => (string) get_option('woocommerce_store_address', ''),
        'address_2' => (string) get_option('woocommerce_store_address_2', ''),
        'city' => (string) get_option('woocommerce_store_city', ''),
        'postcode' => (string) get_option('woocommerce_store_postcode', ''),
        'country' => $country,
        'state' => $state,
    ];

    if ($address['address_1'] === '' && $address['address_2'] === '' && $address['city'] === '') {
        return '';
    }

    $formatted = $wc->countries->get_formatted_address($address, '، ');
    $formatted = wp_strip_all_tags((string) $formatted);
    $formatted = trim((string) preg_replace('/\s+/u', ' ', $formatted));

    return $formatted;
}

/**
 * WooCommerce store postal address as separate lines (for invoice header).
 *
 * @return list<string>
 */
function zerti_wc_get_store_postal_address_lines(): array
{
    if (!class_exists('WooCommerce') || !function_exists('WC')) {
        return [];
    }
    $wc = WC();
    if (!$wc || !$wc->countries) {
        return [];
    }

    $default = (string) get_option('woocommerce_default_country', '');
    $country = $default;
    $state = '';
    if (strpos($default, ':') !== false) {
        $split = explode(':', $default, 2);
        $country = $split[0];
        $state = $split[1] ?? '';
    }

    $address = [
        'address_1' => (string) get_option('woocommerce_store_address', ''),
        'address_2' => (string) get_option('woocommerce_store_address_2', ''),
        'city' => (string) get_option('woocommerce_store_city', ''),
        'postcode' => (string) get_option('woocommerce_store_postcode', ''),
        'country' => $country,
        'state' => $state,
    ];

    if ($address['address_1'] === '' && $address['address_2'] === '' && $address['city'] === '') {
        return [];
    }

    $formatted = $wc->countries->get_formatted_address($address, "\n");
    $formatted = wp_strip_all_tags((string) $formatted);
    $parts = preg_split('/\r\n|\r|\n/u', $formatted);
    if (!is_array($parts)) {
        return [];
    }

    $lines = [];
    foreach ($parts as $part) {
        $part = trim((string) preg_replace('/\s+/u', ' ', (string) $part));
        if ($part !== '') {
            $lines[] = $part;
        }
    }

    if ($lines === []) {
        $plain = zerti_wc_get_store_address_plain();
        if ($plain !== '') {
            $lines[] = $plain;
        }
    }

    return $lines;
}

/**
 * Store address + contact lines for the invoice header (WooCommerce / WordPress settings).
 *
 * @return list<string>
 */
function zerti_shop_invoice_seller_contact_lines(): array
{
    $lines = [];

    $site_url = untrailingslashit((string) home_url('/'));
    if ($site_url !== '') {
        $lines[] = sprintf(
            /* translators: %s: store website URL */
            __('Website: %s', 'zerti'),
            $site_url
        );
    }

    $postal_lines = zerti_wc_get_store_postal_address_lines();
    if ($postal_lines !== []) {
        $postal_display = implode('، ', array_map(
            static function (string $line): string {
                return zerti_to_persian_digits($line);
            },
            $postal_lines
        ));
        $lines[] = sprintf(
            /* translators: %s: store postal address from WooCommerce settings */
            __('Postal address: %s', 'zerti'),
            $postal_display
        );
    }

    $phone = '';
    if (function_exists('wc_get_store_phone')) {
        $phone = trim((string) wc_get_store_phone());
    }
    if ($phone === '') {
        $phone = trim((string) get_option('woocommerce_store_phone', ''));
    }
    if ($phone !== '') {
        $lines[] = sprintf(
            /* translators: %s: store phone number */
            __('Phone: %s', 'zerti'),
            zerti_to_persian_digits($phone)
        );
    }

    $email = trim((string) get_option('woocommerce_email_from_address', ''));
    if ($email === '') {
        $email = trim((string) get_option('admin_email', ''));
    }
    if ($email !== '') {
        $lines[] = sprintf(
            /* translators: %s: store email address */
            __('Email: %s', 'zerti'),
            $email
        );
    }

    /**
     * Filters seller address/contact lines under the invoice header.
     *
     * @param list<string> $lines
     */
    return apply_filters('zerti_shop_invoice_seller_contact_lines', array_values($lines));
}

/**
 * Path to bundled Font Awesome webfonts directory.
 */
function zerti_fontawesome_webfonts_dir(): string
{
    return plugin_dir_path(ZERTI_PLUGIN_FILE) . 'assets/fontawesome/webfonts/';
}

/**
 * Bundled Font Awesome webfonts ship with the plugin; ensure the directory exists.
 */
function zerti_ensure_fontawesome_webfonts(): void
{
    $dir = zerti_fontawesome_webfonts_dir();
    if (!is_dir($dir)) {
        wp_mkdir_p($dir);
    }
}

/**
 * Admin sidebar: size and align the ZERTI menu logo on every admin screen.
 */
function zerti_enqueue_admin_menu_styles(): void
{
    wp_enqueue_style(
        'zerti-admin-menu',
        plugins_url('assets/css/zerti-admin-menu.css', ZERTI_PLUGIN_FILE),
        [],
        ZERTI_VERSION
    );
}

/**
 * Register top-level admin menu item.
 */
function zerti_register_admin_menu(): void
{
    add_menu_page(
        'zertiAdmin',
        'ZERTI',
        zerti_admin_menu_capability(),
        ZERTI_ADMIN_PAGE_SLUG,
        'zerti_render_admin_page',
        zerti_admin_menu_logo_url(),
        99
    );
}

/**
 * Fewer distractions on the ZERTI admin screen.
 */
function zerti_admin_init_strip_notices(): void
{
    if (!is_admin() || empty($_GET['page']) || sanitize_key(wp_unslash((string) $_GET['page'])) !== ZERTI_ADMIN_PAGE_SLUG) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- admin screen detection only.
        return;
    }
    remove_all_actions('admin_notices');
    remove_all_actions('all_admin_notices');
}

/**
 * Convert English digits to Persian digits (Latin → Persian mapping only).
 */
function zerti_to_persian_digits_latin(string $value): string
{
    return strtr($value, [
        '0' => '۰',
        '1' => '۱',
        '2' => '۲',
        '3' => '۳',
        '4' => '۴',
        '5' => '۵',
        '6' => '۶',
        '7' => '۷',
        '8' => '۸',
        '9' => '۹',
    ]);
}

/**
 * Convert English digits to locale-appropriate digits for ZERTI admin UI.
 */
function zerti_to_persian_digits(string $value): string
{
    return zerti_localize_digits($value);
}

/**
 * Convert Persian digits to English digits for search/sort helper data.
 */
function zerti_to_latin_digits(string $value): string
{
    return strtr($value, [
        '۰' => '0',
        '۱' => '1',
        '۲' => '2',
        '۳' => '3',
        '۴' => '4',
        '۵' => '5',
        '۶' => '6',
        '۷' => '7',
        '۸' => '8',
        '۹' => '9',
        '٠' => '0',
        '١' => '1',
        '٢' => '2',
        '٣' => '3',
        '٤' => '4',
        '٥' => '5',
        '٦' => '6',
        '٧' => '7',
        '٨' => '8',
        '٩' => '9',
        '०' => '0',
        '१' => '1',
        '२' => '2',
        '३' => '3',
        '४' => '4',
        '५' => '5',
        '६' => '6',
        '७' => '7',
        '८' => '8',
        '९' => '9',
        '๐' => '0',
        '๑' => '1',
        '๒' => '2',
        '๓' => '3',
        '๔' => '4',
        '๕' => '5',
        '๖' => '6',
        '๗' => '7',
        '๘' => '8',
        '๙' => '9',
    ]);
}

/**
 * Convert English digits to Devanagari digits (Hindi).
 */
function zerti_to_devanagari_digits(string $value): string
{
    return strtr($value, [
        '0' => '०',
        '1' => '१',
        '2' => '२',
        '3' => '३',
        '4' => '४',
        '5' => '५',
        '6' => '६',
        '7' => '७',
        '8' => '८',
        '9' => '९',
    ]);
}

/**
 * Convert English digits to Thai digits.
 */
function zerti_to_thai_digits(string $value): string
{
    return strtr($value, [
        '0' => '๐',
        '1' => '๑',
        '2' => '๒',
        '3' => '๓',
        '4' => '๔',
        '5' => '๕',
        '6' => '๖',
        '7' => '๗',
        '8' => '๘',
        '9' => '๙',
    ]);
}

/**
 * Convert English digits to Arabic-Indic digits.
 */
function zerti_to_arabic_digits(string $value): string
{
    return strtr($value, [
        '0' => '٠',
        '1' => '١',
        '2' => '٢',
        '3' => '٣',
        '4' => '٤',
        '5' => '٥',
        '6' => '٦',
        '7' => '٧',
        '8' => '٨',
        '9' => '٩',
    ]);
}

/**
 * Parse a WooCommerce monetary value into a float (any active currency / separators / Persian digits).
 */
function zerti_wc_parse_amount($value): float
{
    if ($value === null || $value === '') {
        return 0.0;
    }
    if (is_int($value) || is_float($value)) {
        $n = (float) $value;

        return is_finite($n) ? $n : 0.0;
    }

    $s = trim((string) $value);
    if ($s === '') {
        return 0.0;
    }

    $s = zerti_to_latin_digits($s);
    // Strip currency words/symbols; keep digits, separators, and sign.
    $s = preg_replace('/[^\d.,+\-]/u', '', $s) ?? '';
    if ($s === '' || $s === '-' || $s === '+' || $s === '.' || $s === ',') {
        return 0.0;
    }

    if (function_exists('wc_format_decimal')) {
        $s = wc_format_decimal($s, wc_get_price_decimals(), false);
    }

    $n = (float) $s;

    return is_finite($n) ? $n : 0.0;
}

/**
 * Sanitized HTML (e.g. wc_price) with ASCII digits converted to Persian.
 */
function zerti_html_persian_digits(string $html): string
{
    return zerti_html_localized_digits($html);
}

/**
 * Decode HTML entities inside WooCommerce currency symbol markup.
 */
function zerti_decode_wc_price_currency_symbol(string $html): string
{
    if ($html === '') {
        return '';
    }

    $decoded = preg_replace_callback(
        '/(<span class="woocommerce-Price-currencySymbol">)(.*?)(<\/span>)/s',
        static function (array $matches): string {
            return $matches[1] . html_entity_decode($matches[2], ENT_QUOTES | ENT_HTML5, 'UTF-8') . $matches[3];
        },
        $html
    );

    return is_string($decoded) ? $decoded : $html;
}

/**
 * Whether the store currency is Persian (Toman / Rial) — amount should precede currency in RTL UI.
 */
function zerti_wc_is_persian_currency(): bool
{
    if (!function_exists('get_woocommerce_currency')) {
        return false;
    }

    $code = strtoupper((string) get_woocommerce_currency());
    $persian_codes = apply_filters(
        'zerti_wc_persian_currency_codes',
        ['IRR', 'IRT', 'IRHT', 'IRHR']
    );

    return in_array($code, array_map('strtoupper', (array) $persian_codes), true);
}

/**
 * WooCommerce price_format for ZERTI: amount then currency (Persian), else currency then amount.
 */
function zerti_wc_price_format_string(): string
{
    return zerti_wc_is_persian_currency() ? '%2$s %1$s' : '%1$s %2$s';
}

/**
 * Wrapper class isolating price direction in the RTL admin shell.
 */
function zerti_wc_price_wrapper_class(): string
{
    return zerti_wc_is_persian_currency() ? 'zerti-wc-price-rtl' : 'zerti-wc-price-ltr';
}

/**
 * WooCommerce formatted price HTML with Persian digits.
 */
function zerti_wc_price_html_persian($amount): string
{
    if (!function_exists('wc_price')) {
        return '';
    }
    $html = wc_price($amount, [
        'echo' => false,
        'price_format' => zerti_wc_price_format_string(),
    ]);
    $inner = is_string($html) ? zerti_decode_wc_price_currency_symbol($html) : '';

    return zerti_html_persian_digits(
        '<span class="' . esc_attr(zerti_wc_price_wrapper_class()) . '">' . $inner . '</span>'
    );
}

/**
 * Plain number for ZERTI admin UI (always normalizes to the active digit script).
 */
function zerti_format_admin_number($value): string
{
    return zerti_localize_digits((string) $value);
}

/**
 * WooCommerce price HTML for ZERTI admin UI (respects Regional Settings digit format).
 */
function zerti_format_admin_price_html($amount): string
{
    if (!function_exists('wc_price')) {
        return '';
    }

    $html = wc_price($amount, [
        'echo' => false,
        'price_format' => zerti_wc_price_format_string(),
    ]);
    $inner = is_string($html) ? zerti_decode_wc_price_currency_symbol($html) : '';

    return zerti_html_localized_digits(
        '<span class="' . esc_attr(zerti_wc_price_wrapper_class()) . '">' . $inner . '</span>'
    );
}

/**
 * Localized label for a WooCommerce order status slug (ZERTI admin UI).
 */
function zerti_wc_order_status_label_fa(string $status): string
{
    $status = sanitize_key(str_replace('wc-', '', $status));
    if ($status === '') {
        return '';
    }

    $labels = [
        'pending' => __('Pending payment', 'zerti'),
        'processing' => __('Processing', 'zerti'),
        'on-hold' => __('Pending review', 'zerti'),
        'completed' => __('Completed', 'zerti'),
        'cancelled' => __('Cancelled', 'zerti'),
        'refunded' => __('Refund', 'zerti'),
        'failed' => __('Failed payment', 'zerti'),
        'draft' => __('Draft', 'zerti'),
        'checkout-draft' => __('Checkout draft', 'zerti'),
    ];

    if (isset($labels[$status])) {
        return $labels[$status];
    }

    /**
     * Filters the order status label for unknown/custom slugs.
     *
     * @param string|null $label Resolved label or null to use default fallback.
     * @param string      $status Order status slug.
     */
    $filtered = apply_filters('zerti_wc_order_status_label_fa', null, $status);
    if (is_string($filtered) && $filtered !== '') {
        return $filtered;
    }

    if (function_exists('wc_get_order_statuses')) {
        $wc_key = 'wc-' . $status;
        $wc_statuses = wc_get_order_statuses();
        if (isset($wc_statuses[$wc_key]) && is_string($wc_statuses[$wc_key]) && $wc_statuses[$wc_key] !== '') {
            return (string) $wc_statuses[$wc_key];
        }
    }

    /* translators: %s: WooCommerce order status slug (often English). */
    return sprintf(__('Status: %s', 'zerti'), $status);
}

/**
 * Default English titles for core WooCommerce payment gateways.
 *
 * @return array<string, string> Gateway ID => default title (lowercase).
 */
function zerti_wc_payment_gateway_default_titles(): array
{
    return [
        'cod' => 'cash on delivery',
        'bacs' => 'direct bank transfer',
        'cheque' => 'check payments',
    ];
}

/**
 * Localized label for a WooCommerce payment gateway (ZERTI admin UI).
 */
function zerti_wc_payment_gateway_label_fa(string $gateway_id, string $gateway_title = ''): string
{
    $gateway_id = sanitize_key($gateway_id);
    $gateway_title = trim($gateway_title);
    $title_norm = strtolower($gateway_title);

    $title_labels = [
        'cash on delivery' => __('Cash on delivery', 'zerti'),
        'direct bank transfer' => __('Direct bank transfer', 'zerti'),
        'check payments' => __('Check payments', 'zerti'),
    ];

    if ($title_norm !== '' && isset($title_labels[$title_norm])) {
        return $title_labels[$title_norm];
    }

    $id_labels = [
        'cod' => __('Cash on delivery', 'zerti'),
        'bacs' => __('Direct bank transfer', 'zerti'),
        'cheque' => __('Check payments', 'zerti'),
    ];
    $default_titles = zerti_wc_payment_gateway_default_titles();

    if ($gateway_id !== '' && isset($id_labels[$gateway_id])) {
        $default_title = $default_titles[$gateway_id] ?? '';
        if ($title_norm === '' || ($default_title !== '' && $title_norm === $default_title)) {
            return $id_labels[$gateway_id];
        }
    }

    /**
     * Filters the payment gateway label for unknown/custom gateways.
     *
     * @param string|null $label Resolved label or null to use default fallback.
     * @param string      $gateway_id WooCommerce gateway ID.
     * @param string      $gateway_title Stored or registered gateway title.
     */
    $filtered = apply_filters('zerti_wc_payment_gateway_label_fa', null, $gateway_id, $gateway_title);
    if (is_string($filtered) && $filtered !== '') {
        return $filtered;
    }

    if ($gateway_title !== '') {
        return $gateway_title;
    }
    if ($gateway_id !== '') {
        return $gateway_id;
    }

    return '—';
}

/**
 * Grid/display label for a payment gateway (localized + digit formatting).
 */
function zerti_wc_payment_gateway_display_label(string $gateway_id, string $gateway_title = ''): string
{
    $label = zerti_wc_payment_gateway_label_fa($gateway_id, $gateway_title);
    if ($label === '' || $label === '—') {
        return '—';
    }

    return function_exists('zerti_localize_digits')
        ? zerti_localize_digits($label)
        : zerti_to_persian_digits($label);
}

/**
 * Default Persian labels for standard WooCommerce address field keys.
 *
 * @return array<string, string>
 */
function zerti_wc_default_address_field_labels(): array
{
    return [
        'first_name' => __('Name', 'zerti'),
        'last_name' => __('Last name', 'zerti'),
        'company' => __('Company', 'zerti'),
        'address_1' => __('Address (line 1)', 'zerti'),
        'address_2' => __('Address (line 2)', 'zerti'),
        'city' => __('City', 'zerti'),
        'state' => __('State', 'zerti'),
        'postcode' => __('Postal code', 'zerti'),
        'country' => __('Country', 'zerti'),
        'email' => __('Email address', 'zerti'),
        'phone' => __('Phone', 'zerti'),
        'mobile' => __('Mobile', 'zerti'),
    ];
}

/**
 * Whether the effective ZERTI locale prefers Persian/Arabic script in display names.
 */
function zerti_wc_prefers_local_script_locale(?string $locale = null): bool
{
    $locale = $locale ?? (function_exists('zerti_get_effective_locale') ? zerti_get_effective_locale() : get_locale());

    return in_array($locale, ['fa_IR', 'fa_AF', 'ar', 'ar_SA', 'ar_EG', 'ar_SY', 'ar_IQ'], true)
        || zerti_str_starts_with($locale, 'fa_')
        || zerti_str_starts_with($locale, 'ar_');
}

/**
 * Prefer the localized script segment from WooCommerce bilingual labels (e.g. "Tehran (تهران)").
 */
function zerti_wc_prefer_local_script_display_name(string $name, ?string $locale = null): string
{
    $name = trim($name);
    if ($name === '' || !zerti_wc_prefers_local_script_locale($locale)) {
        return $name;
    }

    if (preg_match('/\(([^)]+)\)\s*$/u', $name, $matches)) {
        $inner = trim((string) $matches[1]);
        if ($inner !== '' && preg_match('/[\x{0600}-\x{06FF}\x{0750}-\x{077F}]/u', $inner)) {
            return $inner;
        }
    }

    return $name;
}

/**
 * Cached WooCommerce countries list for display (per request).
 *
 * @return array<string, string>
 */
function zerti_wc_countries_lookup(): array
{
    static $cache = null;

    if (is_array($cache)) {
        return $cache;
    }

    if (!class_exists('WC_Countries')) {
        $cache = [];

        return $cache;
    }

    try {
        $cache = (new WC_Countries())->get_countries();
    } catch (Throwable $e) {
        $cache = [];
    }

    if (!is_array($cache)) {
        $cache = [];
    }

    return $cache;
}

/**
 * Cached WooCommerce states for one country (per request).
 *
 * @return array<string, string>
 */
function zerti_wc_states_lookup(string $country_code): array
{
    static $cache = [];

    $country_code = sanitize_text_field($country_code);
    if ($country_code === '') {
        return [];
    }

    if (isset($cache[$country_code])) {
        return $cache[$country_code];
    }

    if (!class_exists('WC_Countries')) {
        $cache[$country_code] = [];

        return $cache[$country_code];
    }

    try {
        $states = (new WC_Countries())->get_states($country_code);
        $cache[$country_code] = is_array($states) ? $states : [];
    } catch (Throwable $e) {
        $cache[$country_code] = [];
    }

    return $cache[$country_code];
}

/**
 * Country name for display in ZERTI (admin locale + WooCommerce data).
 */
function zerti_wc_localized_country_name(string $country_code, ?string $locale = null): string
{
    static $name_cache = [];

    $country_code = sanitize_text_field($country_code);
    if ($country_code === '') {
        return '';
    }

    $locale = $locale ?? (function_exists('zerti_get_effective_locale') ? zerti_get_effective_locale() : get_locale());
    $cache_key = $locale . "\0" . strtoupper($country_code);
    if (isset($name_cache[$cache_key])) {
        return $name_cache[$cache_key];
    }

    $countries = zerti_wc_countries_lookup();
    $name = isset($countries[$country_code]) ? trim((string) $countries[$country_code]) : $country_code;
    $name = zerti_wc_prefer_local_script_display_name($name, $locale);

    if ($country_code === 'IR' && $name === 'Iran') {
        $translated = __('Iran', 'zerti');
        if ($translated !== '' && $translated !== 'Iran') {
            $name = $translated;
        }
    }

    /**
     * Filters a localized WooCommerce country name shown in ZERTI.
     *
     * @param string $name         Display name.
     * @param string $country_code ISO 3166-1 alpha-2.
     * @param string $locale       Locale used for resolution.
     */
    $name = (string) apply_filters('zerti_wc_localized_country_name', $name, $country_code, $locale);
    $name_cache[$cache_key] = $name;

    return $name;
}

/**
 * State/province name for display in ZERTI (admin locale + WooCommerce data).
 */
function zerti_wc_localized_state_name(string $country_code, string $state_code, ?string $locale = null): string
{
    static $name_cache = [];

    $country_code = sanitize_text_field($country_code);
    $state_code = sanitize_text_field($state_code);
    if ($country_code === '' || $state_code === '') {
        return $state_code;
    }

    $locale = $locale ?? (function_exists('zerti_get_effective_locale') ? zerti_get_effective_locale() : get_locale());
    $cache_key = $locale . "\0" . strtoupper($country_code) . "\0" . $state_code;
    if (isset($name_cache[$cache_key])) {
        return $name_cache[$cache_key];
    }

    $states = zerti_wc_states_lookup($country_code);
    if (!isset($states[$state_code])) {
        $name_cache[$cache_key] = $state_code;

        return $state_code;
    }

    $name = trim((string) $states[$state_code]);
    $name = zerti_wc_prefer_local_script_display_name($name, $locale);

    /**
     * Filters a localized WooCommerce state name shown in ZERTI.
     *
     * @param string $name         Display name.
     * @param string $state_code   State code.
     * @param string $country_code ISO country code.
     * @param string $locale       Locale used for resolution.
     */
    $name = (string) apply_filters('zerti_wc_localized_state_name', $name, $state_code, $country_code, $locale);
    $name_cache[$cache_key] = $name;

    return $name;
}

/**
 * Resolve a human-readable label for a billing/shipping field on an order.
 */
function zerti_wc_order_address_field_label(WC_Order $order, string $context, string $field_key): string
{
    $context = $context === 'shipping' ? 'shipping' : 'billing';
    $field_key = sanitize_key($field_key);

    if ($field_key === 'email' || strpos($field_key, 'email') !== false) {
        return __('Email address', 'zerti');
    }

    if (function_exists('zerti_is_zerti_context') && zerti_is_zerti_context()) {
        $defaults = zerti_wc_default_address_field_labels();
        if (isset($defaults[$field_key])) {
            return $defaults[$field_key];
        }
    }

    $checkout = function_exists('WC') && WC() ? WC()->checkout() : null;
    if ($checkout) {
        $checkout_fields = $checkout->get_checkout_fields($context);
        if (is_array($checkout_fields) && isset($checkout_fields[$field_key]['label'])) {
            $label = wp_strip_all_tags((string) $checkout_fields[$field_key]['label']);
            if ($label !== '') {
                return $label;
            }
        }
    }

    if (function_exists('WC')) {
        $country = $context === 'billing'
            ? (string) $order->get_billing_country()
            : (string) $order->get_shipping_country();
        $address_fields = WC()->countries->get_address_fields($country, $context . '_');
        $full_key = $context . '_' . $field_key;
        if (isset($address_fields[$full_key]['label'])) {
            $label = wp_strip_all_tags((string) $address_fields[$full_key]['label']);
            if ($label !== '') {
                return $label;
            }
        }
    }

    $defaults = zerti_wc_default_address_field_labels();
    if (isset($defaults[$field_key])) {
        return $defaults[$field_key];
    }

    $human = str_replace(['_', '-'], ' ', $field_key);

    return $human !== '' ? $human : $field_key;
}

/**
 * Format a customer field value for display in ZERTI admin.
 */
function zerti_wc_order_format_customer_field_value(string $field_key, string $value): string
{
    $value = trim($value);
    if ($value === '') {
        return '';
    }

    if ($field_key === 'email' || strpos($field_key, 'email') !== false) {
        return $value;
    }

    $short_key = preg_replace('/^(billing|shipping)_/', '', $field_key) ?? $field_key;
    if ($short_key === 'country' && preg_match('/^[A-Z]{2}$/i', $value)) {
        $value = zerti_wc_localized_country_name($value);
    }

    return zerti_to_persian_digits($value);
}

/**
 * Whether order meta is an internal/hidden billing/shipping key (not for display).
 */
function zerti_wc_order_customer_meta_is_internal(string $meta_key): bool
{
    $key = strtolower(ltrim($meta_key, '_'));
    $internal = [
        'billing_address_index',
        'shipping_address_index',
    ];
    if (in_array($key, $internal, true)) {
        return true;
    }
    if (strpos($key, 'wc_order_attribution') === 0) {
        return true;
    }

    return false;
}

/**
 * Normalize order meta value to a display string.
 *
 * @param mixed $value Meta value.
 */
function zerti_wc_order_meta_value_to_string($value): string
{
    if ($value === null || $value === false) {
        return '';
    }
    if (is_scalar($value)) {
        return trim((string) $value);
    }
    if (is_array($value)) {
        $parts = [];
        foreach ($value as $part) {
            if (is_scalar($part)) {
                $s = trim((string) $part);
                if ($s !== '') {
                    $parts[] = $s;
                }
            }
        }

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

    return '';
}

/**
 * Read one billing/shipping field from a WC_Order (getter, address array, or meta).
 */
function zerti_wc_order_get_address_field_value(WC_Order $order, string $context, string $field_key): string
{
    $context = $context === 'shipping' ? 'shipping' : 'billing';
    $field_key = sanitize_key($field_key);
    if ($field_key === '') {
        return '';
    }

    $getter = 'get_' . $context . '_' . $field_key;
    $has_wc_getter = method_exists($order, $getter);
    if ($has_wc_getter) {
        $raw = $order->{$getter}();
        $value = zerti_wc_order_meta_value_to_string($raw);
        if ($value !== '') {
            return $value;
        }
    }

    $address = $order->get_address($context);
    if (is_array($address) && isset($address[$field_key])) {
        $value = zerti_wc_order_meta_value_to_string($address[$field_key]);
        if ($value !== '') {
            return $value;
        }
    }

    // Standard WC address fields must use getters only; get_meta() on internal keys triggers notices.
    if ($has_wc_getter) {
        return '';
    }

    foreach (['_' . $context . '_' . $field_key, $context . '_' . $field_key] as $meta_key) {
        $value = zerti_wc_order_meta_value_to_string($order->get_meta($meta_key, true));
        if ($value !== '') {
            return $value;
        }
    }

    return '';
}

/**
 * Expand country/state codes to readable names when possible.
 */
function zerti_wc_order_format_address_code_value(WC_Order $order, string $context, string $field_key, string $value): string
{
    if ($value === '') {
        return $value;
    }

    $context = $context === 'shipping' ? 'shipping' : 'billing';
    $country = $context === 'billing'
        ? (string) $order->get_billing_country()
        : (string) $order->get_shipping_country();

    if ($field_key === 'country') {
        return zerti_wc_localized_country_name($value);
    }

    if ($field_key === 'state' && $country !== '') {
        return zerti_wc_localized_state_name($country, $value);
    }

    return $value;
}

/**
 * Standard WooCommerce address line field keys (not email / custom contact).
 *
 * @return list<string>
 */
function zerti_wc_order_standard_address_field_keys(): array
{
    return ['first_name', 'last_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country'];
}

/**
 * Whether this order uses a shipping address different from billing (checkout checkbox behaviour).
 */
function zerti_wc_order_ships_to_different_address(WC_Order $order): bool
{
    if (function_exists('wc_ship_to_billing_address_only') && wc_ship_to_billing_address_only()) {
        return false;
    }

    if (method_exists($order, 'needs_shipping_address') && !$order->needs_shipping_address()) {
        return false;
    }

    $compare_keys = array_merge(
        zerti_wc_order_standard_address_field_keys(),
        ['phone']
    );

    $has_shipping_value = false;
    $has_difference = false;

    foreach ($compare_keys as $key) {
        $billing_getter = 'get_billing_' . $key;
        $shipping_getter = 'get_shipping_' . $key;
        if (!method_exists($order, $billing_getter) || !method_exists($order, $shipping_getter)) {
            continue;
        }

        $billing_val = strtolower(trim((string) $order->{$billing_getter}()));
        $shipping_val = strtolower(trim((string) $order->{$shipping_getter}()));

        if ($shipping_val !== '') {
            $has_shipping_value = true;
        }
        if ($billing_val !== $shipping_val) {
            $has_difference = true;
        }
    }

    if (!$has_shipping_value) {
        return false;
    }

    /**
     * Filters whether the order ships to an address different from billing.
     *
     * @param bool     $ships_different Detected value.
     * @param WC_Order $order           Order object.
     */
    return (bool) apply_filters('zerti_wc_order_ships_to_different_address', $has_difference, $order);
}

/**
 * Field rows for order detail: one delivery address (billing or shipping) plus billing contact when needed.
 *
 * @return list<array{label:string,value:string,key:string}>
 */
function zerti_wc_order_collect_display_customer_fields(WC_Order $order): array
{
    $ships_different = zerti_wc_order_ships_to_different_address($order);
    $address_context = $ships_different ? 'shipping' : 'billing';
    $rows = zerti_wc_order_collect_customer_fields($order, $address_context);

    if ($ships_different) {
        $seen = [];
        foreach ($rows as $row) {
            $seen[(string) $row['key']] = true;
        }

        $address_keys = zerti_wc_order_standard_address_field_keys();
        $billing_contact_keys = ['email', 'phone', 'mobile'];

        foreach (zerti_wc_order_collect_customer_fields($order, 'billing') as $billing_row) {
            $key = (string) $billing_row['key'];
            if (zerti_wc_order_should_hide_customer_field($key) || isset($seen[$key])) {
                continue;
            }

            $short_key = preg_replace('/^billing_/', '', $key);
            $is_contact = $key === 'customer_id'
                || in_array($short_key, $billing_contact_keys, true)
                || !in_array($short_key, $address_keys, true);

            if (!$is_contact) {
                continue;
            }

            $rows[] = $billing_row;
            $seen[$key] = true;
        }
    }

    $seen_keys = [];
    foreach ($rows as $row) {
        $seen_keys[(string) $row['key']] = true;
    }
    foreach (zerti_wc_order_collect_order_checkout_fields($order) as $order_row) {
        $order_key = (string) $order_row['key'];
        if (zerti_wc_order_should_hide_customer_field($order_key)) {
            continue;
        }
        if (!isset($seen_keys[$order_key])) {
            $rows[] = $order_row;
            $seen_keys[$order_key] = true;
        }
    }

    $rows = array_values(array_filter(
        $rows,
        static function (array $row): bool {
            return !zerti_wc_order_should_hide_customer_field((string) ($row['key'] ?? ''));
        }
    ));

    /**
     * Filters customer rows shown in ZERTI shop order detail (single delivery/contact block).
     *
     * @param list<array{label:string,value:string,key:string}> $rows
     * @param WC_Order                                          $order
     * @param string                                            $address_context `billing` or `shipping`.
     * @param bool                                              $ships_different Whether shipping differs from billing.
     */
    return apply_filters('zerti_wc_order_display_customer_fields', $rows, $order, $address_context, $ships_different);
}

/**
 * Order statuses treated as paid purchases in customer summary stats.
 *
 * @return list<string>
 */
function zerti_customer_paid_order_statuses(): array
{
    $statuses = apply_filters('zerti_customer_paid_order_statuses', ['completed', 'processing']);
    if (!is_array($statuses) || $statuses === []) {
        return ['completed', 'processing'];
    }

    return array_values(array_unique(array_map('strval', $statuses)));
}

/**
 * Count and sum totals for a customer's paid orders (completed / processing by default).
 *
 * @param list<WC_Order> $orders Orders for this customer (any status).
 * @return array{count:int, total:float}
 */
function zerti_customer_paid_order_stats_from_orders(array $orders): array
{
    $paid_statuses = zerti_customer_paid_order_statuses();
    $count = 0;
    $total = 0.0;

    foreach ($orders as $order) {
        if (!is_a($order, 'WC_Order')) {
            continue;
        }
        if (!in_array($order->get_status(), $paid_statuses, true)) {
            continue;
        }
        $count++;
        $total += (float) $order->get_total();
    }

    return [
        'count' => $count,
        'total' => $total,
    ];
}

/**
 * Section heading for the unified customer block on shop order detail.
 */
function zerti_wc_order_display_customer_section_title(WC_Order $order): string
{
    if (zerti_wc_order_ships_to_different_address($order)) {
        return __('Shipping information', 'zerti');
    }

    return __('Customer information', 'zerti');
}

/**
 * Reorder customer field rows so state and city appear before other address lines.
 *
 * @param list<array{label:string,value:string,key:string}> $rows
 * @return list<array{label:string,value:string,key:string}>
 */
function zerti_wc_order_sort_customer_fields_address_priority(array $rows): array
{
    if ($rows === []) {
        return [];
    }

    $address_priority = ['state', 'city', 'address_1', 'address_2', 'postcode', 'country'];
    $extract_suffix = static function (string $key): string {
        if (preg_match('/^(?:billing|shipping|order)_(.+)$/', $key, $m)) {
            return (string) $m[1];
        }

        return $key;
    };

    $address_rows = [];
    $other_rows = [];
    $address_anchor = null;
    foreach ($rows as $row) {
        $row_key = (string) ($row['key'] ?? '');
        $suffix = $extract_suffix($row_key);
        if (in_array($suffix, $address_priority, true) && !isset($address_rows[$suffix])) {
            if ($address_anchor === null) {
                $address_anchor = count($other_rows);
            }
            $address_rows[$suffix] = $row;
        } else {
            $other_rows[] = $row;
        }
    }

    if ($address_rows === []) {
        return $rows;
    }

    $sorted_address = [];
    foreach ($address_priority as $suffix) {
        if (isset($address_rows[$suffix])) {
            $sorted_address[] = $address_rows[$suffix];
        }
    }
    $anchor = $address_anchor ?? count($other_rows);

    return array_merge(
        array_slice($other_rows, 0, $anchor),
        $sorted_address,
        array_slice($other_rows, $anchor)
    );
}

/**
 * Whether a customer field row should be omitted from invoice / invoice-style displays.
 *
 * @param array{label?:string,value?:string,key?:string} $row
 */
function zerti_wc_order_invoice_should_hide_customer_field(array $row): bool
{
    if ((string) ($row['key'] ?? '') === 'customer_id') {
        return true;
    }

    if (
        zerti_wc_order_customer_field_is_location($row)
        && zerti_wc_order_parse_lat_lng_coordinates((string) ($row['value'] ?? '')) !== null
    ) {
        return true;
    }

    return false;
}

/**
 * Customer field rows as shown on the invoice view (excludes customer_id and map coordinates, invoice address order).
 *
 * @return list<array{label:string,value:string,key:string}>
 */
function zerti_wc_order_invoice_display_customer_fields(WC_Order $order): array
{
    $rows = zerti_wc_order_collect_display_customer_fields($order);
    $rows = array_values(array_filter(
        $rows,
        static function (array $row): bool {
            return !zerti_wc_order_invoice_should_hide_customer_field($row);
        }
    ));

    return zerti_wc_order_sort_customer_fields_address_priority($rows);
}

/**
 * Discover billing/shipping field keys for an order (checkout config + stored address + meta).
 *
 * @return list<string>
 */
function zerti_wc_order_address_field_keys(WC_Order $order, string $context): array
{
    $context = $context === 'shipping' ? 'shipping' : 'billing';
    $keys = [];

    if ($context === 'billing') {
        $keys = ['first_name', 'last_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country', 'email', 'phone'];
    } else {
        $keys = ['first_name', 'last_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country', 'phone'];
    }

    if (function_exists('WC') && WC()->checkout()) {
        $checkout_fields = WC()->checkout()->get_checkout_fields($context);
        if (is_array($checkout_fields)) {
            foreach (array_keys($checkout_fields) as $key) {
                $keys[] = sanitize_key((string) $key);
            }
        }
    }

    $address = $order->get_address($context);
    if (is_array($address)) {
        foreach (array_keys($address) as $key) {
            $keys[] = sanitize_key((string) $key);
        }
    }

    $prefix = $context . '_';
    foreach ($order->get_meta_data() as $meta) {
        if (!is_object($meta) || !method_exists($meta, 'get_data')) {
            continue;
        }
        $data = $meta->get_data();
        $meta_key = isset($data['key']) ? (string) $data['key'] : '';
        if ($meta_key === '' || zerti_wc_order_customer_meta_is_internal($meta_key)) {
            continue;
        }
        if (strpos($meta_key, $prefix) === 0) {
            $keys[] = sanitize_key(substr($meta_key, strlen($prefix)));
            continue;
        }
        if (strpos($meta_key, '_' . $prefix) === 0) {
            $keys[] = sanitize_key(substr($meta_key, strlen($prefix) + 1));
        }
    }

    $keys = array_values(array_unique(array_filter($keys)));

    /**
     * Filters address field keys collected for a WooCommerce order in ZERTI.
     *
     * @param list<string> $keys    Field keys (without billing_/shipping_ prefix).
     * @param WC_Order     $order   Order object.
     * @param string       $context `billing` or `shipping`.
     */
    return apply_filters('zerti_wc_order_address_field_keys', $keys, $order, $context);
}

/**
 * Customer field keys (short form, without billing_/shipping_ prefix) hidden in ZERTI order UI.
 *
 * @return list<string>
 */
function zerti_wc_order_hidden_customer_field_keys(): array
{
    /**
     * @param list<string> $keys Field keys to exclude from customer blocks (e.g. state_id, city_id).
     */
    return apply_filters('zerti_wc_order_hidden_customer_field_keys', ['state_id', 'city_id', 'country']);
}

/**
 * Whether a stored customer row key should be hidden (supports billing_state_id, order_city_id, etc.).
 */
function zerti_wc_order_should_hide_customer_field(string $key): bool
{
    $key = sanitize_key($key);
    if ($key === '') {
        return false;
    }

    $short = $key;
    if (preg_match('/^(?:billing|shipping|order)_(.+)$/', $key, $matches)) {
        $short = sanitize_key((string) $matches[1]);
    }

    $hidden = zerti_wc_order_hidden_customer_field_keys();
    if (in_array($short, $hidden, true) || in_array($key, $hidden, true)) {
        return true;
    }

    /**
     * @param bool   $hide Whether to hide this field.
     * @param string $key  Full row key from customer field collection.
     * @param string $short Key without billing_/shipping_/order_ prefix.
     */
    return (bool) apply_filters('zerti_wc_order_should_hide_customer_field', false, $key, $short);
}

/**
 * Collect non-empty customer-related fields for billing or shipping on an order.
 *
 * @return list<array{label:string,value:string,key:string}>
 */
function zerti_wc_order_collect_customer_fields(WC_Order $order, string $context): array
{
    $context = $context === 'shipping' ? 'shipping' : 'billing';
    $rows = [];
    $seen = [];

    foreach (zerti_wc_order_address_field_keys($order, $context) as $field_key) {
        if ($field_key === '' || isset($seen[$field_key]) || zerti_wc_order_should_hide_customer_field($field_key)) {
            continue;
        }
        $raw = zerti_wc_order_get_address_field_value($order, $context, $field_key);
        if ($raw === '') {
            continue;
        }
        $raw = zerti_wc_order_format_address_code_value($order, $context, $field_key, $raw);
        $rows[] = [
            'label' => zerti_wc_order_address_field_label($order, $context, $field_key),
            'value' => zerti_wc_order_format_customer_field_value($field_key, $raw),
            'key' => $context . '_' . $field_key,
        ];
        $seen[$field_key] = true;
    }

    if ($context === 'billing' && (int) $order->get_customer_id() > 0) {
        $customer_id_raw = (int) $order->get_customer_id();
        array_unshift($rows, [
            'label' => __('Customer ID (WooCommerce)', 'zerti'),
            'value' => zerti_to_persian_digits((string) $customer_id_raw),
            'key' => 'customer_id',
            'customer_id' => $customer_id_raw,
        ]);
    }

    /**
     * Filters customer field rows shown in ZERTI shop order detail.
     *
     * @param list<array{label:string,value:string,key:string}> $rows
     * @param WC_Order                                          $order
     * @param string                                            $context `billing` or `shipping`.
     */
    return apply_filters('zerti_wc_order_customer_fields', $rows, $order, $context);
}

/**
 * Collect custom checkout "order" fields stored on the order (excludes order comments).
 *
 * @return list<array{label:string,value:string,key:string}>
 */
function zerti_wc_order_collect_order_checkout_fields(WC_Order $order): array
{
    $rows = [];
    if (!function_exists('WC') || !WC()->checkout()) {
        return $rows;
    }

    $order_fields = WC()->checkout()->get_checkout_fields('order');
    if (!is_array($order_fields)) {
        return $rows;
    }

    foreach ($order_fields as $field_key => $field_def) {
        $field_key = sanitize_key((string) $field_key);
        if ($field_key === '' || $field_key === 'order_comments' || zerti_wc_order_should_hide_customer_field($field_key)) {
            continue;
        }

        $value = '';
        foreach (['_' . $field_key, $field_key] as $meta_key) {
            $value = zerti_wc_order_meta_value_to_string($order->get_meta($meta_key, true));
            if ($value !== '') {
                break;
            }
        }
        if ($value === '') {
            continue;
        }

        $label = '';
        if (is_array($field_def) && isset($field_def['label'])) {
            $label = wp_strip_all_tags((string) $field_def['label']);
        }
        if ($label === '') {
            $label = str_replace(['_', '-'], ' ', $field_key);
        }

        $rows[] = [
            'label' => $label,
            'value' => zerti_wc_order_format_customer_field_value($field_key, $value),
            'key' => 'order_' . $field_key,
        ];
    }

    /**
     * Filters extra checkout order-group field rows in ZERTI shop order detail.
     *
     * @param list<array{label:string,value:string,key:string}> $rows
     * @param WC_Order                                          $order
     */
    return apply_filters('zerti_wc_order_checkout_order_fields', $rows, $order);
}

/**
 * Whether a customer field row is the checkout "your location" (موقعیت شما) field.
 *
 * @param array{label?:string,value?:string,key?:string} $row
 */
function zerti_wc_order_customer_field_is_location(array $row): bool
{
    $label = isset($row['label']) ? (string) $row['label'] : '';
    if ($label !== '' && function_exists('mb_stripos')) {
        if (mb_stripos($label, 'موقعیت', 0, 'UTF-8') !== false) {
            return true;
        }
    } elseif ($label !== '' && stripos($label, 'موقعیت') !== false) {
        return true;
    }

    $key = isset($row['key']) ? strtolower((string) $row['key']) : '';
    foreach (['location', 'geolocation', 'geo_location', 'map_location', 'your_location'] as $needle) {
        if ($needle !== '' && strpos($key, $needle) !== false) {
            return true;
        }
    }

    return false;
}

/**
 * Parse "lat, lng" (Latin or Persian digits) from a location field value.
 *
 * @return array{lat:float, lng:float}|null
 */
function zerti_wc_order_parse_lat_lng_coordinates(string $value): ?array
{
    $value = zerti_to_latin_digits(trim($value));
    $value = preg_replace('/\s+/u', ' ', $value);
    if (!is_string($value) || $value === '') {
        return null;
    }

    if (!preg_match('/(-?\d+(?:\.\d+)?)\s*[,،]\s*(-?\d+(?:\.\d+)?)/u', $value, $matches)) {
        return null;
    }

    $lat = (float) $matches[1];
    $lng = (float) $matches[2];
    if ($lat < -90 || $lat > 90 || $lng < -180 || $lng > 180) {
        return null;
    }

    return [
        'lat' => $lat,
        'lng' => $lng,
    ];
}

/**
 * Google Maps URL for a latitude/longitude pair.
 */
function zerti_wc_order_google_maps_url(float $lat, float $lng): string
{
    return 'https://www.google.com/maps?q=' . rawurlencode(sprintf('%F,%F', $lat, $lng));
}

/**
 * Echo a definition list of customer/order fields for shop order detail.
 *
 * @param list<array{label:string,value:string,key:string}> $rows
 */
function zerti_wc_render_order_customer_field_rows(array $rows): void
{
    if ($rows === []) {
        echo '<p class="text-body-secondary mb-0">—</p>';

        return;
    }
    ?>
    <dl class="zerti-shop-order-customer-fields zerti-shop-order-customer-fields--compact mb-0">
        <?php foreach ($rows as $row) : ?>
            <?php
            $field_key = isset($row['key']) ? (string) $row['key'] : '';
            $short_key = $field_key;
            if (preg_match('/^(?:billing|shipping|order)_(.+)$/', $field_key, $matches)) {
                $short_key = (string) $matches[1];
            }
            $wide_keys = ['address_1', 'address_2', 'company'];
            $is_wide = in_array($short_key, $wide_keys, true);
            $is_customer_id = $field_key === 'customer_id';
            $is_email = $field_key === 'email'
                || $field_key === 'billing_email'
                || $field_key === 'shipping_email'
                || strpos($field_key, 'email') !== false;
            ?>
            <div class="zerti-shop-order-customer-field<?php
            echo $is_wide ? ' zerti-shop-order-customer-field--wide' : '';
            echo $is_customer_id ? ' zerti-shop-order-customer-field--customer-id' : '';
            ?>">
                <dt class="zerti-shop-order-customer-field__label"><?php echo esc_html((string) $row['label']); ?></dt>
                <dd class="zerti-shop-order-customer-field__value mb-0">
                    <?php
                    $customer_id_raw = isset($row['customer_id']) ? (int) $row['customer_id'] : 0;
                    $show_customer_link = $field_key === 'customer_id'
                        && $customer_id_raw > 0
                        && function_exists('zerti_user_can_view_customer_detail')
                        && zerti_user_can_view_customer_detail($customer_id_raw);
                    if ($show_customer_link) :
                        ?>
                        <button
                            type="button"
                            class="btn btn-sm btn-outline-secondary d-inline-flex align-items-center zerti-customer-detail-btn zerti-shop-order-customer-id-btn"
                            data-zerti-customer="<?php echo esc_attr((string) $customer_id_raw); ?>"
                            title="<?php esc_attr_e('View customer details', 'zerti'); ?>"
                        ><?php
                        echo esc_html(
                            sprintf(
                                /* translators: %s: WooCommerce customer user ID */
                                __('View profile: %s', 'zerti'),
                                (string) $row['value']
                            )
                        );
                        ?></button><br>
                    <?php elseif ($is_email) : ?>
                        <span dir="ltr"><?php echo esc_html((string) $row['value']); ?></span>
                    <?php
                    elseif (
                        zerti_wc_order_customer_field_is_location($row)
                        && ($coords = zerti_wc_order_parse_lat_lng_coordinates((string) ($row['value'] ?? ''))) !== null
                    ) :
                        $maps_url = zerti_wc_order_google_maps_url($coords['lat'], $coords['lng']);
                        $location_coords = zerti_to_persian_digits((string) $row['value']);
                        ?>
                        <a
                            href="<?php echo esc_url($maps_url); ?>"
                            class="btn btn-sm btn-outline-secondary d-inline-flex align-items-center zerti-shop-order-location-btn"
                            target="_blank"
                            rel="noopener noreferrer"
                            title="<?php echo esc_attr(sprintf(
                                /* translators: %s: latitude and longitude */
                                __('View location on Google Maps (%s)', 'zerti'),
                                $location_coords
                            )); ?>"
                        ><?php esc_html_e('Show on map', 'zerti'); ?></a>
                    <?php else : ?>
                        <?php echo esc_html((string) $row['value']); ?>
                    <?php endif; ?>
                </dd>
            </div>
        <?php endforeach; ?>
    </dl>
    <?php
}

/**
 * Line tax amount for a WooCommerce order line item.
 */
function zerti_wc_order_line_item_tax_amount($item): float
{
    if (!is_object($item)) {
        return 0.0;
    }
    $total_tax = method_exists($item, 'get_total_tax') ? (float) $item->get_total_tax() : 0.0;
    $subtotal_tax = method_exists($item, 'get_subtotal_tax') ? (float) $item->get_subtotal_tax() : 0.0;

    return $total_tax + $subtotal_tax;
}

/**
 * Line grand total (matches WooCommerce admin line total incl. tax when applicable).
 */
function zerti_wc_order_line_item_line_total($item): float
{
    if (!is_object($item) || !method_exists($item, 'get_total')) {
        return 0.0;
    }
    $total = (float) $item->get_total();
    if (function_exists('wc_prices_include_tax') && wc_prices_include_tax()) {
        return $total;
    }

    return $total + zerti_wc_order_line_item_tax_amount($item);
}

/**
 * Parsed row for the shop order line-items table.
 *
 * @return array{row:int,product_id:int,name:string,unit_price:float,discount:float,tax:float,qty:float,line_total:float}|null
 */
function zerti_wc_order_parse_line_item_row($item, int $row_index): ?array
{
    if (!is_object($item) || !method_exists($item, 'get_type') || $item->get_type() !== 'line_item') {
        return null;
    }
    if (!method_exists($item, 'get_name')) {
        return null;
    }

    $qty = method_exists($item, 'get_quantity') ? (float) $item->get_quantity() : 0.0;
    $subtotal = method_exists($item, 'get_subtotal') ? (float) $item->get_subtotal() : 0.0;
    $total = method_exists($item, 'get_total') ? (float) $item->get_total() : 0.0;
    $discount = max(0.0, $subtotal - $total);
    $unit_price = $qty > 0 ? $subtotal / $qty : 0.0;

    $variation_id = method_exists($item, 'get_variation_id') ? (int) $item->get_variation_id() : 0;
    $product_id = method_exists($item, 'get_product_id') ? (int) $item->get_product_id() : 0;
    $display_id = $variation_id > 0 ? $variation_id : $product_id;

    $sku = '';
    if ($display_id > 0 && function_exists('wc_get_product')) {
        $product = wc_get_product($display_id);
        if ($product && is_a($product, 'WC_Product')) {
            $sku = (string) $product->get_sku();
        }
    }

    return [
        'row' => $row_index,
        'product_id' => $display_id,
        'sku' => $sku,
        'name' => (string) $item->get_name(),
        'unit_price' => $unit_price,
        'discount' => $discount,
        'tax' => zerti_wc_order_line_item_tax_amount($item),
        'qty' => $qty,
        'line_total' => zerti_wc_order_line_item_line_total($item),
    ];
}

/**
 * Echo a price cell for order line tables (dash when zero).
 */
function zerti_wc_render_order_item_price_cell(float $amount): void
{
    if (abs($amount) < 0.00001) {
        echo '<span class="text-body-secondary">—</span>';

        return;
    }
    echo wp_kses_post(zerti_wc_price_html_persian($amount));
}

/**
 * Gregorian date to Julian day number (integer).
 */
function zerti_gregorian_to_julian_day(int $y, int $m, int $d): int
{
    $a = intdiv(14 - $m, 12);
    $y = $y + 4800 - $a;
    $m = $m + 12 * $a - 3;

    return $d + intdiv(153 * $m + 2, 5) + 365 * $y + intdiv($y, 4) - intdiv($y, 100) + intdiv($y, 400) - 32045;
}

/**
 * Julian day number to Gregorian [year, month, day].
 *
 * @return array{0:int,1:int,2:int}
 */
function zerti_julian_day_to_gregorian(int $jdn): array
{
    $a = $jdn + 32044;
    $b = intdiv(4 * $a + 3, 146097);
    $c = $a - intdiv(146097 * $b, 4);
    $d = intdiv(4 * $c + 3, 1461);
    $e = $c - intdiv(1461 * $d, 4);
    $m = intdiv(5 * $e + 2, 153);
    $day = $e - intdiv(153 * $m + 2, 5) + 1;
    $month = $m + 3 - 12 * intdiv($m, 10);
    $year = 100 * $b + $d - 4800 + intdiv($m, 10);

    return [$year, $month, $day];
}

/**
 * Convert Gregorian date to Hijri (tabular Islamic calendar).
 *
 * @return array{0:int,1:int,2:int} [year, month, day]
 */
function zerti_gregorian_to_hijri(int $gy, int $gm, int $gd): array
{
    $jd = zerti_gregorian_to_julian_day($gy, $gm, $gd);
    $l = $jd - 1948440 + 10632;
    $n = intdiv($l - 1, 10631);
    $l = $l - 10631 * $n + 354;
    $j = intdiv(10985 - $l, 5316) * intdiv(50 * $l, 17719)
        + intdiv($l, 5670) * intdiv(43 * $l, 15238);
    $l = $l - intdiv(30 - $j, 15) * intdiv(17719 * $j, 50)
        - intdiv($j, 16) * intdiv(15238 * $j, 43) + 29;
    $hm = intdiv(24 * $l, 709);
    $hd = $l - intdiv(709 * $hm, 24);
    $hy = 30 * $n + $j - 30;

    return [$hy, $hm, $hd];
}

/**
 * Convert Hijri date to Gregorian.
 *
 * @return array{0:int,1:int,2:int} [year, month, day]
 */
function zerti_hijri_to_gregorian(int $hy, int $hm, int $hd): array
{
    $jd = intdiv(11 * $hy + 3, 30) + 354 * $hy + 30 * $hm - intdiv($hm - 1, 2) + $hd + 1948440 - 385;

    return zerti_julian_day_to_gregorian($jd);
}

/**
 * Whether a Hijri year is a leap year (tabular calendar).
 */
function zerti_is_hijri_leap(int $hy): bool
{
    return (11 * $hy + 14) % 30 < 11;
}

/**
 * Length of a Hijri month (tabular calendar).
 */
function zerti_hijri_month_length(int $hy, int $hm): int
{
    if ($hm === 12) {
        return zerti_is_hijri_leap($hy) ? 30 : 29;
    }

    return ($hm % 2 === 1) ? 30 : 29;
}

/**
 * Parse a Hijri (or legacy Gregorian Y-m-d) date string to a Unix timestamp at local midnight.
 */
function zerti_parse_hijri_date_string(string $raw): ?int
{
    $latin = zerti_to_latin_digits(trim($raw));
    if ($latin === '') {
        return null;
    }

    if (preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $latin, $m)) {
        $tz = zerti_get_timezone();
        $dt = DateTimeImmutable::createFromFormat(
            'Y-n-j H:i:s',
            sprintf('%d-%d-%d 00:00:00', (int) $m[1], (int) $m[2], (int) $m[3]),
            $tz
        );
        if ($dt instanceof DateTimeImmutable) {
            return $dt->getTimestamp();
        }

        return null;
    }

    if (!preg_match('/^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})/', $latin, $m)) {
        return null;
    }

    [$gy, $gm, $gd] = zerti_hijri_to_gregorian((int) $m[1], (int) $m[2], (int) $m[3]);
    $tz = zerti_get_timezone();
    $dt = DateTimeImmutable::createFromFormat('Y-n-j H:i:s', sprintf('%d-%d-%d 00:00:00', $gy, $gm, $gd), $tz);
    if ($dt instanceof DateTimeImmutable) {
        return $dt->getTimestamp();
    }

    return null;
}

/**
 * Format timestamp as Hijri date-time string: YYYY/MM/DD HH:MM (Arabic-Indic digits).
 *
 * @param bool $with_seconds When true, appends :SS for live clocks.
 */
function zerti_format_hijri_datetime(int $timestamp, bool $with_seconds = false): string
{
    $dt = new DateTime('@' . $timestamp);
    $dt->setTimezone(zerti_get_timezone());

    $gy = (int) $dt->format('Y');
    $gm = (int) $dt->format('n');
    $gd = (int) $dt->format('j');
    $hour = $dt->format('H');
    $minute = $dt->format('i');
    $second = $dt->format('s');

    [$hy, $hm, $hd] = zerti_gregorian_to_hijri($gy, $gm, $gd);
    $hijri = sprintf('%04d/%02d/%02d %s:%s', $hy, $hm, $hd, $hour, $minute);
    if ($with_seconds) {
        $hijri .= ':' . $second;
    }

    return zerti_localize_digits($hijri);
}

/**
 * Hijri calendar date (YYYY/MM/DD) with locale-appropriate digits for UI labels.
 */
function zerti_format_hijri_date_arabic(DateTimeInterface $dt): string
{
    $local = (new DateTimeImmutable('@' . $dt->getTimestamp()))->setTimezone(zerti_get_timezone());
    $gy = (int) $local->format('Y');
    $gm = (int) $local->format('n');
    $gd = (int) $local->format('j');
    [$hy, $hm, $hd] = zerti_gregorian_to_hijri($gy, $gm, $gd);

    return zerti_localize_digits(sprintf('%04d/%02d/%02d', $hy, $hm, $hd));
}

/**
 * Convert Gregorian date to Jalali date.
 *
 * @return array{0:int,1:int,2:int} [year, month, day]
 */
function zerti_gregorian_to_jalali(int $gy, int $gm, int $gd): array
{
    $gDaysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
    $jDaysInMonth = [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29];

    $gy -= 1600;
    $gm -= 1;
    $gd -= 1;

    $gDayNo = 365 * $gy + intdiv($gy + 3, 4) - intdiv($gy + 99, 100) + intdiv($gy + 399, 400);
    for ($i = 0; $i < $gm; $i++) {
        $gDayNo += $gDaysInMonth[$i];
    }

    // Leap year adjustment for Gregorian calendar.
    if ($gm > 1 && (($gy % 4 === 0 && $gy % 100 !== 0) || ($gy % 400 === 0))) {
        $gDayNo++;
    }

    $gDayNo += $gd;

    $jDayNo = $gDayNo - 79;
    $jNp = intdiv($jDayNo, 12053);
    $jDayNo %= 12053;

    $jy = 979 + 33 * $jNp + 4 * intdiv($jDayNo, 1461);
    $jDayNo %= 1461;

    if ($jDayNo >= 366) {
        $jy += intdiv($jDayNo - 1, 365);
        $jDayNo = ($jDayNo - 1) % 365;
    }

    $jm = 0;
    while ($jm < 11 && $jDayNo >= $jDaysInMonth[$jm]) {
        $jDayNo -= $jDaysInMonth[$jm];
        $jm++;
    }

    $jd = $jDayNo + 1;

    return [$jy, $jm + 1, $jd];
}

/**
 * Convert Jalali date to Gregorian date parts.
 *
 * @return array{0:int, 1:int, 2:int}
 */
function zerti_jalali_to_gregorian(int $jy, int $jm, int $jd): array
{
    $jy += 1595;
    $days = -355668 + (365 * $jy) + (int) floor($jy / 33) * 8 + (int) floor((($jy % 33) + 3) / 4) + $jd;
    $days += $jm < 7 ? ($jm - 1) * 31 : (($jm - 7) * 30) + 186;

    $gy = 400 * (int) floor($days / 146097);
    $days %= 146097;
    if ($days > 36524) {
        $gy += 100 * (int) floor(--$days / 36524);
        $days %= 36524;
        if ($days >= 365) {
            $days++;
        }
    }
    $gy += 4 * (int) floor($days / 1461);
    $days %= 1461;
    if ($days > 365) {
        $gy += (int) floor(($days - 1) / 365);
        $days = ($days - 1) % 365;
    }
    $gd = $days + 1;
    $leap = ($gy % 4 === 0 && $gy % 100 !== 0) || $gy % 400 === 0;
    $month_days = [31, $leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
    $gm = 1;
    foreach ($month_days as $month_day_count) {
        if ($gd <= $month_day_count) {
            break;
        }
        $gd -= $month_day_count;
        $gm++;
    }

    return [$gy, $gm, $gd];
}

/**
 * Parse a Jalali (or legacy Gregorian Y-m-d) date string to a Unix timestamp at local midnight.
 */
function zerti_parse_jalali_date_string(string $raw): ?int
{
    $latin = zerti_to_latin_digits(trim($raw));
    if ($latin === '') {
        return null;
    }

    if (preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $latin, $m)) {
        $tz = zerti_get_timezone();
        $dt = DateTimeImmutable::createFromFormat('Y-n-j H:i:s', sprintf('%d-%d-%d 00:00:00', (int) $m[1], (int) $m[2], (int) $m[3]), $tz);
        if ($dt instanceof DateTimeImmutable) {
            return $dt->getTimestamp();
        }

        return null;
    }

    if (!preg_match('/^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})/', $latin, $m)) {
        return null;
    }

    [$gy, $gm, $gd] = zerti_jalali_to_gregorian((int) $m[1], (int) $m[2], (int) $m[3]);
    $tz = zerti_get_timezone();
    $dt = DateTimeImmutable::createFromFormat('Y-n-j H:i:s', sprintf('%d-%d-%d 00:00:00', $gy, $gm, $gd), $tz);
    if ($dt instanceof DateTimeImmutable) {
        return $dt->getTimestamp();
    }

    return null;
}

/**
 * Saved IANA timezone from ZERTI settings, or empty to use the WordPress site timezone.
 */
function zerti_get_timezone_string(): string
{
    if (!function_exists('zerti_get_plugin_settings')) {
        return '';
    }
    $settings = zerti_get_plugin_settings();
    $tz = isset($settings['general']['timezone']) ? trim((string) $settings['general']['timezone']) : '';
    if ($tz === '' || !in_array($tz, timezone_identifiers_list(), true)) {
        return '';
    }

    return $tz;
}

/**
 * Timezone used for all ZERTI date/time display and range calculations.
 */
function zerti_get_timezone(): DateTimeZone
{
    $tz_string = zerti_get_timezone_string();
    if ($tz_string !== '') {
        try {
            return new DateTimeZone($tz_string);
        } catch (Exception $e) {
            // Fall through to WordPress default.
        }
    }

    return wp_timezone();
}

/**
 * Human-readable label for a timezone identifier (with UTC offset).
 */
function zerti_timezone_label(string $tz_string): string
{
    if ($tz_string === '') {
        return '';
    }
    try {
        $tz = new DateTimeZone($tz_string);
        $now = new DateTimeImmutable('now', $tz);

        return $tz_string . ' (UTC' . $now->format('P') . ')';
    } catch (Exception $e) {
        return $tz_string;
    }
}

/**
 * Label for one timezone <option> (city name + UTC offset).
 */
function zerti_timezone_option_label(string $zone): string
{
    $parts = explode('/', $zone, 2);
    $label = count($parts) === 2 ? str_replace('_', ' ', $parts[1]) : $zone;
    try {
        $tz = new DateTimeZone($zone);
        $now = new DateTimeImmutable('now', $tz);
        $label .= ' (UTC' . $now->format('P') . ')';
    } catch (Exception $e) {
        // Keep city-only label.
    }

    return $label;
}

/**
 * Build grouped timezone identifiers for the settings dropdown.
 *
 * @return array<string, array<string, string>> Continent => [ city label => IANA id ].
 */
function zerti_timezone_groups(): array
{
    $continents = [
        'Africa',
        'America',
        'Antarctica',
        'Arctic',
        'Asia',
        'Atlantic',
        'Australia',
        'Europe',
        'Indian',
        'Pacific',
    ];

    $groups = [];
    foreach (timezone_identifiers_list() as $zone) {
        $parts = explode('/', $zone, 2);
        if (count($parts) !== 2) {
            continue;
        }
        $continent = $parts[0];
        if (!in_array($continent, $continents, true)) {
            continue;
        }
        if (!isset($groups[$continent])) {
            $groups[$continent] = [];
        }
        $groups[$continent][$zone] = zerti_timezone_option_label($zone);
    }

    foreach ($groups as $continent => $zones) {
        asort($groups[$continent], SORT_FLAG_CASE | SORT_STRING);
    }

    return $groups;
}

/**
 * Render the general-settings timezone <select> (grouped IANA list + inherit option).
 */
function zerti_settings_timezone_select(string $current, string $input_name): void
{
    $current = trim($current);
    $wp_tz = wp_timezone_string();
    $groups = zerti_timezone_groups();

    $continent_labels = [
        'Africa' => __('Africa', 'zerti'),
        'America' => __('Americas', 'zerti'),
        'Antarctica' => __('Antarctica', 'zerti'),
        'Arctic' => __('Arctic', 'zerti'),
        'Asia' => __('Asia', 'zerti'),
        'Atlantic' => __('Atlantic Ocean', 'zerti'),
        'Australia' => __('Australia', 'zerti'),
        'Europe' => __('Europe', 'zerti'),
        'Indian' => __('Indian Ocean', 'zerti'),
        'Pacific' => __('Pacific Ocean', 'zerti'),
    ];

    echo '<select class="form-select zerti-admin-select zerti-settings-timezone-select" id="zertiGeneralTimezone" name="' . esc_attr($input_name) . '">';
    printf(
        '<option value=""%s>%s</option>',
        selected($current, '', false),
        esc_html(
            sprintf(
                /* translators: %s: WordPress site timezone label */
                __('Same as WordPress timezone (%s)', 'zerti'),
                zerti_timezone_label($wp_tz)
            )
        )
    );

    foreach ($continent_labels as $continent => $continent_label) {
        if (empty($groups[$continent])) {
            continue;
        }
        echo '<optgroup label="' . esc_attr($continent_label) . '">';
        foreach ($groups[$continent] as $zone => $label) {
            printf(
                '<option value="%s"%s>%s</option>',
                esc_attr($zone),
                selected($current, $zone, false),
                esc_html($label)
            );
        }
        echo '</optgroup>';
    }

    echo '</select>';
}

/**
 * Shop orders list — within 24h show hours ago; otherwise Jalali date only (no clock).
 */
function zerti_format_shop_order_list_date(int $timestamp): string
{
    if ($timestamp <= 0) {
        return '—';
    }

    $diff = time() - $timestamp;
    if ($diff >= 0 && $diff < DAY_IN_SECONDS) {
        $hours = (int) floor($diff / HOUR_IN_SECONDS);
        if ($hours < 1) {
            return __('Less than 1 hour ago', 'zerti');
        }

        return sprintf(
            /* translators: %s: hour count (Persian digits). */
            __('%s hours ago', 'zerti'),
            zerti_to_persian_digits((string) $hours)
        );
    }

    $dt = new DateTime('@' . $timestamp);
    $dt->setTimezone(zerti_get_timezone());

    return zerti_format_date($dt);
}

/**
 * Format timestamp as Gregorian date-time (Y/m/d H:i[:s], Latin digits).
 */
function zerti_format_gregorian_datetime(int $timestamp, bool $with_seconds = false): string
{
    $dt = new DateTime('@' . $timestamp);
    $dt->setTimezone(zerti_get_timezone());
    $formatted = $with_seconds ? $dt->format('Y/m/d H:i:s') : $dt->format('Y/m/d H:i');

    return zerti_localize_digits($formatted);
}

/**
 * Gregorian calendar date (Y/m/d) for UI labels.
 */
function zerti_format_gregorian_date(DateTimeInterface $dt): string
{
    $local = (new DateTimeImmutable('@' . $dt->getTimestamp()))->setTimezone(zerti_get_timezone());

    return zerti_localize_digits($local->format('Y/m/d'));
}

/**
 * Format timestamp using the configured admin calendar.
 */
function zerti_format_datetime(int $timestamp, bool $with_seconds = false): string
{
    $calendar = function_exists('zerti_get_calendar') ? zerti_get_calendar() : 'jalali';
    if ($calendar === 'gregorian') {
        return zerti_format_gregorian_datetime($timestamp, $with_seconds);
    }
    if ($calendar === 'hijri') {
        return zerti_format_hijri_datetime($timestamp, $with_seconds);
    }

    return zerti_format_jalali_datetime($timestamp, $with_seconds);
}

/**
 * Format a date using the configured admin calendar.
 */
function zerti_format_date(DateTimeInterface $dt): string
{
    $calendar = function_exists('zerti_get_calendar') ? zerti_get_calendar() : 'jalali';
    if ($calendar === 'gregorian') {
        return zerti_format_gregorian_date($dt);
    }
    if ($calendar === 'hijri') {
        return zerti_format_hijri_date_arabic($dt);
    }

    return zerti_format_jalali_date_persian($dt);
}

/**
 * Split a formatted admin datetime string into date and clock parts.
 *
 * @return array{date:string,time:string}
 */
function zerti_split_datetime_display(string $datetime): array
{
    return zerti_split_jalali_datetime_display($datetime);
}

/**
 * Parse a Gregorian Y/m/d or Y-m-d date string to Unix timestamp at local midnight.
 */
function zerti_parse_gregorian_date_string(string $raw): ?int
{
    $latin = zerti_to_latin_digits(trim($raw));
    if ($latin === '') {
        return null;
    }

    if (!preg_match('/^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})/', $latin, $m)) {
        return null;
    }

    $tz = zerti_get_timezone();
    $dt = DateTimeImmutable::createFromFormat(
        'Y-n-j H:i:s',
        sprintf('%d-%d-%d 00:00:00', (int) $m[1], (int) $m[2], (int) $m[3]),
        $tz
    );
    if ($dt instanceof DateTimeImmutable) {
        return $dt->getTimestamp();
    }

    return null;
}

/**
 * Parse a date string from admin UI using the configured calendar.
 */
function zerti_parse_admin_date_string(string $raw): ?int
{
    $calendar = function_exists('zerti_get_calendar') ? zerti_get_calendar() : 'jalali';
    if ($calendar === 'gregorian') {
        return zerti_parse_gregorian_date_string($raw);
    }
    if ($calendar === 'hijri') {
        return zerti_parse_hijri_date_string($raw);
    }

    return zerti_parse_jalali_date_string($raw);
}

/**
 * Convert a formatted admin date filter value to a WooCommerce date_created range.
 */
function zerti_admin_date_display_to_created_range(string $value): string
{
    $latin = zerti_to_latin_digits($value);
    if (!preg_match('/^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})/', $latin, $m)) {
        return '';
    }

    if (function_exists('zerti_is_jalali_calendar') && zerti_is_jalali_calendar()) {
        [$gy, $gm, $gd] = zerti_jalali_to_gregorian((int) $m[1], (int) $m[2], (int) $m[3]);
    } elseif (function_exists('zerti_is_hijri_calendar') && zerti_is_hijri_calendar()) {
        [$gy, $gm, $gd] = zerti_hijri_to_gregorian((int) $m[1], (int) $m[2], (int) $m[3]);
    } else {
        $gy = (int) $m[1];
        $gm = (int) $m[2];
        $gd = (int) $m[3];
    }

    $tz = zerti_get_timezone();
    $start = new DateTimeImmutable(sprintf('%04d-%02d-%02d 00:00:00', $gy, $gm, $gd), $tz);
    $end = $start->setTime(23, 59, 59);

    return $start->format('Y-m-d H:i:s') . '...' . $end->format('Y-m-d H:i:s');
}

/**
 * Placeholder for admin date inputs.
 */
function zerti_admin_date_placeholder(): string
{
    $calendar = function_exists('zerti_get_calendar') ? zerti_get_calendar() : 'jalali';
    if ($calendar === 'gregorian') {
        return '2024/01/01';
    }
    if ($calendar === 'hijri') {
        return '١٤٤٥/٠١/٠١';
    }

    return '۱۴۰۳/۰۱/۰۱';
}

/**
 * HTML input type for admin date fields (text + popup, or native date).
 */
function zerti_admin_date_input_type(): string
{
    return function_exists('zerti_is_gregorian_calendar') && zerti_is_gregorian_calendar() ? 'date' : 'text';
}

/**
 * Data attribute(s) for admin date fields.
 */
function zerti_admin_date_input_data_attr(): string
{
    $calendar = function_exists('zerti_get_calendar') ? zerti_get_calendar() : 'jalali';
    if ($calendar === 'gregorian') {
        return 'data-zerti-date data-zerti-gregorian-date';
    }
    if ($calendar === 'hijri') {
        return 'data-zerti-hijri-date';
    }

    return 'data-zerti-jalali-date';
}

/**
 * Format timestamp as Jalali date-time string: YYYY/MM/DD HH:MM (Persian digits).
 *
 * @param bool $with_seconds When true, appends :SS for live clocks.
 */
function zerti_format_jalali_datetime(int $timestamp, bool $with_seconds = false): string
{
    $dt = new DateTime('@' . $timestamp);
    $dt->setTimezone(zerti_get_timezone());

    $gy = (int) $dt->format('Y');
    $gm = (int) $dt->format('n');
    $gd = (int) $dt->format('j');
    $hour = $dt->format('H');
    $minute = $dt->format('i');

    [$jy, $jm, $jd] = zerti_gregorian_to_jalali($gy, $gm, $gd);
    if ($with_seconds) {
        $second = $dt->format('s');
        $jalali = sprintf('%04d/%02d/%02d %s:%s:%s', $jy, $jm, $jd, $hour, $minute, $second);
    } else {
        $jalali = sprintf('%04d/%02d/%02d %s:%s', $jy, $jm, $jd, $hour, $minute);
    }

    return zerti_to_persian_digits($jalali);
}

/**
 * Split a Jalali datetime string into date and clock parts for sidebar display.
 *
 * @return array{date:string,time:string}
 */
function zerti_split_jalali_datetime_display(string $datetime): array
{
    $datetime = trim($datetime);
    if ($datetime === '' || $datetime === '—') {
        return ['date' => '—', 'time' => ''];
    }

    $space = strpos($datetime, ' ');
    if ($space === false) {
        return ['date' => $datetime, 'time' => ''];
    }

    return [
        'date' => substr($datetime, 0, $space),
        'time' => substr($datetime, $space + 1),
    ];
}

/**
 * Jalali calendar date (YYYY/MM/DD) with Persian digits for UI labels (e.g. charts).
 */
function zerti_format_jalali_date_persian(DateTimeInterface $dt): string
{
    $local = (new DateTimeImmutable('@' . $dt->getTimestamp()))->setTimezone(zerti_get_timezone());
    $gy = (int) $local->format('Y');
    $gm = (int) $local->format('n');
    $gd = (int) $local->format('j');
    [$jy, $jm, $jd] = zerti_gregorian_to_jalali($gy, $gm, $gd);

    return zerti_to_persian_digits(sprintf('%04d/%02d/%02d', $jy, $jm, $jd));
}

