<?php

namespace WPSN\Integrations\CustomFields;

use WPSN\Domain\SeoDocumentContext;

defined('ABSPATH') || exit;

/**
 * CustomFieldResolver
 *
 * Provides a unified way to fetch values from popular field sources:
 * - ACF (posts/terms + Options Pages)
 * - Meta Box
 * - JetEngine (incl. Custom Meta Storage via JetEngine API; falls back to WP meta)
 * - Pods (best-effort)
 * - WooCommerce product properties (best-effort)
 * - Native postmeta/termmeta
 */
final class CustomFieldResolver
{
    public const PROVIDER_AUTO = 'auto';
    public const PROVIDER_META = 'meta';
    public const PROVIDER_CF   = 'cf';
    public const PROVIDER_ACF  = 'acf';
    public const PROVIDER_MB   = 'mb';
    public const PROVIDER_JET  = 'jet';
    public const PROVIDER_PODS = 'pods';
    public const PROVIDER_WOO  = 'woo';
    public const PROVIDER_ACF_OPTION = 'acfopt';


    public function resolveForContext(SeoDocumentContext $ctx, string $provider, string $key): string
    {
        $provider = strtolower(trim($provider));
        $key = trim($key);
        if ($key === '') {
            return '';
        }

        // Context-free providers.
        if ($provider === self::PROVIDER_ACF_OPTION) {
            return $this->resolveAcfOption($key);
        }

        if ($ctx->isSingular()) {
            return $this->resolvePostField((int) $ctx->objectId, $provider, $key);
        }

        if ($ctx->isTerm()) {
            $tax = (string) ($ctx->objectSubType ?? '');
            return $this->resolveTermField((int) $ctx->objectId, $tax, $provider, $key);
        }

        return '';
    }

    public function resolvePostField(int $postId, string $provider, string $key): string
    {
        if ($postId <= 0) {
            return '';
        }

        $provider = strtolower(trim($provider));
        $key = trim($key);

        if ($provider === self::PROVIDER_CF) {
            $provider = self::PROVIDER_AUTO;
        }

        if ($provider === self::PROVIDER_AUTO) {
            // Try provider-specific sources first.
            $val = $this->resolvePostField($postId, self::PROVIDER_ACF, $key);
            if ($val !== '') {
                return $val;
            }
            $val = $this->resolvePostField($postId, self::PROVIDER_MB, $key);
            if ($val !== '') {
                return $val;
            }
            $val = $this->resolvePostField($postId, self::PROVIDER_JET, $key);
            if ($val !== '') {
                return $val;
            }
            $val = $this->resolvePostField($postId, self::PROVIDER_PODS, $key);
            if ($val !== '') {
                return $val;
            }
            return $this->resolvePostField($postId, self::PROVIDER_META, $key);
        }

        if ($provider === self::PROVIDER_ACF) {
            return $this->resolveAcfPost($postId, $key);
        }

        if ($provider === self::PROVIDER_MB) {
            return $this->resolveMetaBoxPost($postId, $key);
        }

        if ($provider === self::PROVIDER_JET) {
            return $this->resolveJetEnginePost($postId, $key);
        }

        if ($provider === self::PROVIDER_PODS) {
            return $this->resolvePodsPost($postId, $key);
        }

        if ($provider === self::PROVIDER_WOO) {
            return $this->resolveWooPost($postId, $key);
        }

        if ($provider === self::PROVIDER_ACF_OPTION) {
            return $this->resolveAcfOption($key);
        }

        // meta (default)
        $raw = get_post_meta($postId, $key, true);
        return $this->stringify($raw);
    }

    public function resolveTermField(int $termId, string $taxonomy, string $provider, string $key): string
    {
        if ($termId <= 0) {
            return '';
        }

        $taxonomy = (string) $taxonomy;
        $provider = strtolower(trim($provider));
        $key = trim($key);

        if ($provider === self::PROVIDER_CF) {
            $provider = self::PROVIDER_AUTO;
        }

        if ($provider === self::PROVIDER_AUTO) {
            $val = $this->resolveTermField($termId, $taxonomy, self::PROVIDER_ACF, $key);
            if ($val !== '') {
                return $val;
            }
            $val = $this->resolveTermField($termId, $taxonomy, self::PROVIDER_MB, $key);
            if ($val !== '') {
                return $val;
            }
            $val = $this->resolveTermField($termId, $taxonomy, self::PROVIDER_JET, $key);
            if ($val !== '') {
                return $val;
            }
            $val = $this->resolveTermField($termId, $taxonomy, self::PROVIDER_PODS, $key);
            if ($val !== '') {
                return $val;
            }
            return $this->resolveTermField($termId, $taxonomy, self::PROVIDER_META, $key);
        }

        if ($provider === self::PROVIDER_ACF) {
            return $this->resolveAcfTerm($termId, $taxonomy, $key);
        }

        if ($provider === self::PROVIDER_MB) {
            return $this->resolveMetaBoxTerm($termId, $taxonomy, $key);
        }

        if ($provider === self::PROVIDER_JET) {
            return $this->resolveJetEngineTerm($termId, $taxonomy, $key);
        }

        if ($provider === self::PROVIDER_PODS) {
            return $this->resolvePodsTerm($termId, $taxonomy, $key);
        }

        if ($provider === self::PROVIDER_ACF_OPTION) {
            return $this->resolveAcfOption($key);
        }

        // meta (default)
        $raw = get_term_meta($termId, $key, true);
        return $this->stringify($raw);
    }

    private function resolveAcfPost(int $postId, string $key): string
    {
        if (!function_exists('get_field')) {
            return '';
        }

        // Support nested ACF keys (groups / repeaters) using colon paths:
        // - %acf:group:field%
        // - %acf:repeater:subfield% (joined)
        [$rootKey, $path] = $this->splitPathKey($key);

        // get_field($selector, $post_id, $format_value)
        $raw = get_field($rootKey, $postId, false);
        if ($raw === false) {
            $raw = '';
        }
        if (!empty($path)) {
            $vals = $this->extractValuesByPath($raw, $path);
            if (count($vals) === 1) {
                $raw = $vals[0];
            } elseif (count($vals) > 1) {
                $raw = $vals;
            } else {
                $raw = '';
            }
        }

        return $this->stringify($raw);
    }

    private function resolveAcfTerm(int $termId, string $taxonomy, string $key): string
    {
        if (!function_exists('get_field')) {
            return '';
        }

        // Support nested ACF keys (groups / repeaters) using colon paths.
        [$rootKey, $path] = $this->splitPathKey($key);

        // ACF commonly uses taxonomy_termId (e.g. category_3) as the reference.
        $ref = $taxonomy !== '' ? ($taxonomy . '_' . $termId) : ('term_' . $termId);
        $raw = get_field($rootKey, $ref, false);
        if ($raw === false) {
            $raw = '';
        }
        if (!empty($path)) {
            $vals = $this->extractValuesByPath($raw, $path);
            if (count($vals) === 1) {
                $raw = $vals[0];
            } elseif (count($vals) > 1) {
                $raw = $vals;
            } else {
                $raw = '';
            }
        }

        return $this->stringify($raw);
    }

    private function resolveAcfOption(string $key): string
    {
        if (!function_exists('get_field')) {
            return '';
        }

        // Support nested ACF keys (groups / repeaters) using colon paths.
        [$rootKey, $path] = $this->splitPathKey($key);

        // ACF options pages: get_field($selector, 'option'|'options', $format_value)
        $raw = get_field($rootKey, 'option', false);
        if ($raw === false) {
            $raw = '';
        }

        if (!empty($path)) {
            $vals = $this->extractValuesByPath($raw, $path);
            if (count($vals) === 1) {
                $raw = $vals[0];
            } elseif (count($vals) > 1) {
                $raw = $vals;
            } else {
                $raw = '';
            }
        }

        return $this->stringify($raw);
    }

    /**
     * Split a colon-path key into root key + path segments.
     *
     * Examples:
     * - "hero" => ["hero", []]
     * - "group:field" => ["group", ["field"]]
     * - "repeater:row:sub" => ["repeater", ["row","sub"]]
     *
     * @return array{0:string,1:array<int,string>}
     */
    private function splitPathKey(string $key): array
    {
        $key = trim($key);
        if ($key === '' || strpos($key, ':') === false) {
            return [$key, []];
        }

        $parts = explode(':', $key);
        $parts = array_values(array_filter(array_map('trim', $parts), static fn($v) => $v !== ''));
        if (count($parts) <= 1) {
            return [$key, []];
        }

        $root = (string) array_shift($parts);
        return [$root, $parts];
    }

    /**
     * Extract values by path segments from (possibly nested) arrays.
     *
     * - If $value is a list (repeater rows), it will map each row and join values.
     * - If $value is an associative array (group field), it will drill down.
     *
     * @param mixed $value
     * @param array<int,string> $path
     * @return array<int,mixed>
     */
    private function extractValuesByPath($value, array $path): array
    {
        if (empty($path)) {
            return [$value];
        }

        $seg = (string) array_shift($path);
        if ($seg === '') {
            return $this->extractValuesByPath($value, $path);
        }

        // Repeater / list: apply to each item using the same segment.
        if (is_array($value) && $this->isListArray($value)) {
            $out = [];
            foreach ($value as $row) {
                $vals = $this->extractValuesByPath($row, array_merge([$seg], $path));
                foreach ($vals as $v) {
                    $out[] = $v;
                }
            }
            return $out;
        }

        // Associative array: drill down.
        if (is_array($value)) {
            if (array_key_exists($seg, $value)) {
                return $this->extractValuesByPath($value[$seg], $path);
            }
            return [];
        }

        // Object with property.
        if (is_object($value) && isset($value->{$seg})) {
            return $this->extractValuesByPath($value->{$seg}, $path);
        }

        return [];
    }

    /**
     * @param array<mixed> $arr
     */
    private function isListArray(array $arr): bool
    {
        $i = 0;
        foreach ($arr as $k => $_v) {
            if ($k !== $i) {
                return false;
            }
            $i++;
        }
        return true;
    }

    private function resolveMetaBoxPost(int $postId, string $key): string
    {
if (!function_exists('rwmb_meta')) {
    return '';
}

// Support nested keys (groups / repeaters) using colon paths:
// - %mb:group:field%
// - %mb:repeater:subfield% (joined)
[$rootKey, $path] = $this->splitPathKey($key);

// rwmb_meta($key, $args, $object_id)
$raw = rwmb_meta($rootKey, [], $postId);

if (!empty($path)) {
    $vals = $this->extractValuesByPath($raw, $path);
    if (count($vals) === 1) {
        $raw = $vals[0];
    } elseif (count($vals) > 1) {
        $raw = $vals;
    } else {
        $raw = '';
    }
}

return $this->stringify($raw);
    }

    private function resolveMetaBoxTerm(int $termId, string $taxonomy, string $key): string
    {
if (!function_exists('rwmb_meta')) {
    return '';
}

// Support nested keys (groups / repeaters) using colon paths:
// - %mb:group:field%
// - %mb:repeater:subfield% (joined)
[$rootKey, $path] = $this->splitPathKey($key);

// Meta Box supports terms with object_type => term in many setups.
$raw = rwmb_meta($rootKey, ['object_type' => 'term', 'taxonomy' => $taxonomy], $termId);

if (!empty($path)) {
    $vals = $this->extractValuesByPath($raw, $path);
    if (count($vals) === 1) {
        $raw = $vals[0];
    } elseif (count($vals) > 1) {
        $raw = $vals;
    } else {
        $raw = '';
    }
}

return $this->stringify($raw);
    }

    private function resolveJetEnginePost(int $postId, string $key): string
    {
// Best-effort: JetEngine may store meta in custom tables. Prefer JetEngine APIs when available,
// then fallback to native get_post_meta() (some JetEngine setups intercept it).
[$rootKey, $path] = $this->splitPathKey($key);

$raw = null;
if (function_exists('jet_engine')) {
    $raw = $this->jetEngineGetPostMetaRaw($postId, $rootKey);
}

if ($raw === null) {
    $raw = get_post_meta($postId, $rootKey, true);
}

if (!empty($path)) {
    $vals = $this->extractValuesByPath($raw, $path);
    if (count($vals) === 1) {
        $raw = $vals[0];
    } elseif (count($vals) > 1) {
        $raw = $vals;
    } else {
        $raw = '';
    }
}

return $this->stringify($raw);
    }

    private function resolveJetEngineTerm(int $termId, string $taxonomy, string $key): string
    {
// Best-effort: JetEngine often uses native term meta, but may also use custom storage.
[$rootKey, $path] = $this->splitPathKey($key);

$raw = null;
if (function_exists('jet_engine')) {
    $raw = $this->jetEngineGetTermMetaRaw($termId, $taxonomy, $rootKey);
}

if ($raw === null) {
    $raw = get_term_meta($termId, $rootKey, true);
}

if (!empty($path)) {
    $vals = $this->extractValuesByPath($raw, $path);
    if (count($vals) === 1) {
        $raw = $vals[0];
    } elseif (count($vals) > 1) {
        $raw = $vals;
    } else {
        $raw = '';
    }
}

return $this->stringify($raw);
    }

    private function resolvePodsPost(int $postId, string $key): string
    {
        if ($postId <= 0) {
            return '';
        }

        $key = trim($key);
        if ($key === '' || !function_exists('pods_field')) {
            return '';
        }

        $pod = '';
        $fieldPath = $key;

        // Explicit pod: %pods:pod.field%
        if (strpos($key, '.') !== false) {
            [$pod, $fieldPath] = explode('.', $key, 2);
            $pod = trim((string) $pod);
            $fieldPath = trim((string) $fieldPath);
        }

        if ($pod === '') {
            $pod = (string) get_post_type($postId);
        }

        if ($pod === '' || $fieldPath === '') {
            return '';
        }

        [$rootKey, $path] = $this->splitPathKey($fieldPath);

        $raw = null;
        try {
            // pods_field( $pod, $id = false, $name = null, $single = false )
            $raw = pods_field($pod, $postId, $rootKey, true);
        } catch (\Throwable $e) {
            $raw = null;
        }

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

        if (!empty($path)) {
            $vals = $this->extractValuesByPath($raw, $path);
            if (count($vals) === 1) {
                $raw = $vals[0];
            } elseif (count($vals) > 1) {
                $raw = $vals;
            } else {
                $raw = '';
            }
        }

        return $this->stringify($raw);
    }

    private function resolvePodsTerm(int $termId, string $taxonomy, string $key): string
    {
        if ($termId <= 0) {
            return '';
        }

        $taxonomy = (string) $taxonomy;
        $key = trim($key);
        if ($key === '' || !function_exists('pods_field')) {
            return '';
        }

        $pod = '';
        $fieldPath = $key;

        // Explicit pod: %pods:pod.field%
        if (strpos($key, '.') !== false) {
            [$pod, $fieldPath] = explode('.', $key, 2);
            $pod = trim((string) $pod);
            $fieldPath = trim((string) $fieldPath);
        }

        if ($pod === '') {
            $pod = $taxonomy;
        }

        if ($pod === '' || $fieldPath === '') {
            return '';
        }

        [$rootKey, $path] = $this->splitPathKey($fieldPath);

        $raw = null;
        try {
            $raw = pods_field($pod, $termId, $rootKey, true);
        } catch (\Throwable $e) {
            $raw = null;
        }

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

        if (!empty($path)) {
            $vals = $this->extractValuesByPath($raw, $path);
            if (count($vals) === 1) {
                $raw = $vals[0];
            } elseif (count($vals) > 1) {
                $raw = $vals;
            } else {
                $raw = '';
            }
        }

        return $this->stringify($raw);
    }

    private function resolveWooPost(int $postId, string $key): string
    {
        if ($postId <= 0) {
            return '';
        }

        $key = trim($key);
        if ($key === '' || !function_exists('wc_get_product')) {
            return '';
        }

        try {
            $product = wc_get_product($postId);
        } catch (\Throwable $e) {
            $product = null;
        }

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

        // Allow %woo:attr:pa_color% / %woo:attribute:pa_size%
        [$rootKey, $path] = $this->splitPathKey($key);
        $rk = strtolower(trim((string) $rootKey));

        if (in_array($rk, ['attr', 'attribute'], true)) {
            $attr = isset($path[0]) ? trim((string) $path[0]) : '';
            if ($attr === '') {
                return '';
            }
            try {
                $raw = $product->get_attribute($attr);
            } catch (\Throwable $e) {
                $raw = '';
            }
            return $this->stringify($raw);
        }

        $raw = '';
        try {
            switch ($rk) {
                case 'id':
                    $raw = method_exists($product, 'get_id') ? $product->get_id() : $postId;
                    break;
                case 'sku':
                    $raw = method_exists($product, 'get_sku') ? $product->get_sku() : '';
                    break;
                case 'price':
                    $raw = method_exists($product, 'get_price') ? $product->get_price() : '';
                    break;
                case 'regular_price':
                    $raw = method_exists($product, 'get_regular_price') ? $product->get_regular_price() : '';
                    break;
                case 'sale_price':
                    $raw = method_exists($product, 'get_sale_price') ? $product->get_sale_price() : '';
                    break;
                case 'currency':
                    $raw = function_exists('get_woocommerce_currency') ? get_woocommerce_currency() : '';
                    break;
                case 'stock_status':
                    $raw = method_exists($product, 'get_stock_status') ? $product->get_stock_status() : '';
                    break;
                case 'stock_quantity':
                    $raw = method_exists($product, 'get_stock_quantity') ? $product->get_stock_quantity() : '';
                    break;
                case 'in_stock':
                    $raw = method_exists($product, 'is_in_stock') ? (bool) $product->is_in_stock() : '';
                    break;
                case 'on_sale':
                    $raw = method_exists($product, 'is_on_sale') ? (bool) $product->is_on_sale() : '';
                    break;
                case 'type':
                    $raw = method_exists($product, 'get_type') ? $product->get_type() : '';
                    break;
                case 'permalink':
                case 'url':
                    $raw = method_exists($product, 'get_permalink') ? $product->get_permalink() : '';
                    break;
                case 'short_description':
                case 'shortdesc':
                    $raw = method_exists($product, 'get_short_description') ? $product->get_short_description() : '';
                    break;
                case 'description':
                    $raw = method_exists($product, 'get_description') ? $product->get_description() : '';
                    break;
                case 'weight':
                    $raw = method_exists($product, 'get_weight') ? $product->get_weight() : '';
                    break;
                case 'dimensions':
                    $raw = method_exists($product, 'get_dimensions') ? $product->get_dimensions() : '';
                    break;
                case 'average_rating':
                    $raw = method_exists($product, 'get_average_rating') ? $product->get_average_rating() : '';
                    break;
                case 'rating_count':
                    $raw = method_exists($product, 'get_rating_count') ? $product->get_rating_count() : '';
                    break;
                case 'review_count':
                    $raw = method_exists($product, 'get_review_count') ? $product->get_review_count() : '';
                    break;
                default:
                    $raw = '';
                    break;
            }
        } catch (\Throwable $e) {
            $raw = '';
        }

        return $this->stringify($raw);
    }

/**
 * Try to fetch JetEngine post meta using JetEngine APIs (supports custom tables).
 *
 * Returns null when no JetEngine API could provide a value.
 *
 * @param int $postId
 * @param string $key
 * @return mixed|null
 */
private function jetEngineGetPostMetaRaw(int $postId, string $key)
{
    $key = trim($key);
    if ($postId <= 0 || $key === '') {
        return null;
    }

    try {
        $je = jet_engine();
    } catch (\Throwable $e) {
        return null;
    }

    if (!is_object($je)) {
        return null;
    }

    // Preferred: JetEngine Listings Data API. Supports Custom Meta Storage.
    if (isset($je->listings) && is_object($je->listings) && isset($je->listings->data) && is_object($je->listings->data) && method_exists($je->listings->data, 'get_meta')) {
        $obj = get_post($postId);
        if ($obj instanceof \WP_Post) {
            try {
                $raw = $je->listings->data->get_meta($key, $obj);
            } catch (\Throwable $e) {
                $raw = null;
            }
            if ($this->jetEngineIsUsefulValue($raw)) {
                return $raw;
            }
        }
    }

    $candidates = [];
    $this->collectJetEngineCandidates($je, $candidates);

    $methods = [
        'get_value',
        'get_meta',
        'get_meta_value',
        'get_field_value',
        'get_prop',
        'get',
        'get_raw',
    ];

    $argVariants = [
        [$key],
        [$key, $postId],
        [$postId, $key],
        [$key, $postId, 'post'],
        [$key, $postId, ['object_type' => 'post']],
        [$key, ['object_id' => $postId, 'object_type' => 'post']],
    ];

    foreach ($candidates as $obj) {
        $raw = $this->callCandidateMethods($obj, $methods, $argVariants);
        if ($raw !== null) {
            return $raw;
        }
    }

    return null;
}

/**
 * Try to fetch JetEngine term meta using JetEngine APIs (best-effort).
 *
 * @param int $termId
 * @param string $taxonomy
 * @param string $key
 * @return mixed|null
 */
private function jetEngineGetTermMetaRaw(int $termId, string $taxonomy, string $key)
{
    $key = trim($key);
    if ($termId <= 0 || $key === '') {
        return null;
    }

    $taxonomy = (string) $taxonomy;

    try {
        $je = jet_engine();
    } catch (\Throwable $e) {
        return null;
    }

    if (!is_object($je)) {
        return null;
    }

    // Preferred: JetEngine Listings Data API. Supports Custom Meta Storage.
    if (isset($je->listings) && is_object($je->listings) && isset($je->listings->data) && is_object($je->listings->data) && method_exists($je->listings->data, 'get_meta')) {
        $termObj = null;
        if ($taxonomy !== '') {
            $termObj = get_term($termId, $taxonomy);
        } else {
            $termObj = get_term($termId);
        }

        if ($termObj && !is_wp_error($termObj) && $termObj instanceof \WP_Term) {
            try {
                $raw = $je->listings->data->get_meta($key, $termObj);
            } catch (\Throwable $e) {
                $raw = null;
            }
            if ($this->jetEngineIsUsefulValue($raw)) {
                return $raw;
            }
        }
    }

    $candidates = [];
    $this->collectJetEngineCandidates($je, $candidates);

    $methods = [
        'get_value',
        'get_meta',
        'get_meta_value',
        'get_field_value',
        'get_prop',
        'get',
        'get_raw',
    ];

    $argVariants = [
        [$key],
        [$key, $termId, 'term', $taxonomy],
        [$key, $termId, 'term'],
        [$key, $termId],
        [$termId, $key],
        [$key, ['object_type' => 'term', 'taxonomy' => $taxonomy], $termId],
        [$key, ['object_type' => 'term'], $termId],
        [$key, ['object_id' => $termId, 'object_type' => 'term', 'taxonomy' => $taxonomy]],
    ];

    foreach ($candidates as $obj) {
        $raw = $this->callCandidateMethods($obj, $methods, $argVariants);
        if ($raw !== null) {
            return $raw;
        }
    }

    return null;
}

/**
 * Collect candidate JetEngine objects that may expose meta getters.
 *
 * @param object $je
 * @param array<int,object> $out
 */
private function collectJetEngineCandidates(object $je, array &$out): void
{
    $seen = [];

    $add = static function ($obj) use (&$out, &$seen): void {
        if (!is_object($obj)) {
            return;
        }
        $h = function_exists('spl_object_hash') ? spl_object_hash($obj) : null;
        if (is_string($h) && $h !== '') {
            if (isset($seen[$h])) {
                return;
            }
            $seen[$h] = 1;
        }
        $out[] = $obj;
    };

    // JetEngine root.
    $add($je);

    // Meta boxes module candidates.
    if (isset($je->meta_boxes) && is_object($je->meta_boxes)) {
        $add($je->meta_boxes);

        foreach (['data', 'storage', 'meta_storage', 'meta'] as $p) {
            if (isset($je->meta_boxes->{$p}) && is_object($je->meta_boxes->{$p})) {
                $add($je->meta_boxes->{$p});
            }
        }

        // Nested storage inside data.
        if (isset($je->meta_boxes->data) && is_object($je->meta_boxes->data)) {
            foreach (['storage', 'meta_storage', 'meta'] as $p) {
                if (isset($je->meta_boxes->data->{$p}) && is_object($je->meta_boxes->data->{$p})) {
                    $add($je->meta_boxes->data->{$p});
                }
            }
        }
    }

    // Listings data module sometimes has meta getters.
    if (isset($je->listings) && is_object($je->listings) && isset($je->listings->data) && is_object($je->listings->data)) {
        $add($je->listings->data);
        foreach (['storage', 'meta_storage', 'meta'] as $p) {
            if (isset($je->listings->data->{$p}) && is_object($je->listings->data->{$p})) {
                $add($je->listings->data->{$p});
            }
        }
    }

    // Modules registry (best-effort).
    if (isset($je->modules) && is_object($je->modules)) {
        $add($je->modules);
        if (method_exists($je->modules, 'get_module')) {
            foreach (['meta-boxes', 'meta_boxes', 'meta-box', 'custom-content-types', 'custom_content_types'] as $slug) {
                try {
                    $m = $je->modules->get_module($slug);
                    if (is_object($m)) {
                        $add($m);
                        foreach (['data', 'storage', 'meta_storage', 'meta'] as $p) {
                            if (isset($m->{$p}) && is_object($m->{$p})) {
                                $add($m->{$p});
                            }
                        }
                    }
                } catch (\Throwable $e) {
                    // ignore
                }
            }
        }
    }
}

/**
 * Call candidate methods using multiple arg variants.
 *
 * @param object $obj
 * @param array<int,string> $methods
 * @param array<int,array<int,mixed>> $argVariants
 * @return mixed|null
 */
private function callCandidateMethods(object $obj, array $methods, array $argVariants)
{
    foreach ($methods as $method) {
        if (!is_string($method) || $method === '' || !method_exists($obj, $method)) {
            continue;
        }

        foreach ($argVariants as $args) {
            if (!is_array($args)) {
                continue;
            }
            try {
                // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
                $raw = call_user_func_array([$obj, $method], $args);
            } catch (\Throwable $e) {
                continue;
            }

            if ($this->jetEngineIsUsefulValue($raw)) {
                return $raw;
            }
        }
    }

    return null;
}

/**
 * Determine whether a JetEngine getter return value looks "useful".
 *
 * @param mixed $raw
 */
private function jetEngineIsUsefulValue($raw): bool
{
    if ($raw === null) {
        return false;
    }
    if ($raw === false) {
        return false;
    }
    if (function_exists('is_wp_error') && is_wp_error($raw)) {
        return false;
    }
    if (is_string($raw) && trim($raw) === '') {
        return false;
    }
    if (is_array($raw) && $raw === []) {
        return false;
    }
    return true;
}

    /**
     * @param mixed $value
     */
    private function stringify($value): string
    {
        if ($value === null) {
            return '';
        }

        if (is_bool($value)) {
            return $value ? '1' : '0';
        }

        if (is_numeric($value)) {
            return (string) $value;
        }

        if (is_string($value)) {
            return trim($value);
        }

        if (is_array($value)) {
            $flat = $this->flattenArray($value);
            $flat = array_filter($flat, static fn ($v) => is_string($v) && trim($v) !== '');
            $flat = array_map(static fn ($v) => trim((string) $v), $flat);
            $flat = array_slice(array_values(array_unique($flat)), 0, 30);
            return trim(implode(', ', $flat));
        }

        if (is_object($value)) {
            // Common objects: WP_Post, WP_Term, etc.
            if (isset($value->ID) && is_numeric($value->ID)) {
                return (string) $value->ID;
            }
            if (isset($value->name) && is_string($value->name)) {
                return trim($value->name);
            }
            if (method_exists($value, '__toString')) {
                return trim((string) $value);
            }
        }

        return '';
    }

    /**
     * @param array<mixed> $arr
     * @return array<int, string>
     */
    private function flattenArray(array $arr): array
    {
        $out = [];
        foreach ($arr as $v) {
            if (is_string($v) || is_numeric($v) || is_bool($v)) {
                $out[] = (string) $v;
            } elseif (is_array($v)) {
                foreach ($this->flattenArray($v) as $vv) {
                    $out[] = $vv;
                }
            } elseif (is_object($v)) {
                if (isset($v->name) && is_string($v->name)) {
                    $out[] = (string) $v->name;
                } elseif (method_exists($v, '__toString')) {
                    $out[] = (string) $v;
                }
            }
        }
        return $out;
    }
}
