<?php
declare(strict_types=1);

namespace Help4BuilderSuite\Modules\Fields;

use Help4BuilderSuite\Modules\Commerce\ProductRepository;
use WP_Post;
use WP_Term;
use WP_User;

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

/**
 * Resolves public, structured data for Builder widgets and field shortcodes.
 */
final class DynamicDataResolver
{
    private const MAX_PATH_DEPTH = 12;
    private const MAX_TRANSFORMS = 8;

    private ProductRepository $products;

    public function __construct(?ProductRepository $products = null)
    {
        $this->products = $products ?? new ProductRepository();
    }

    /**
     * @return array<string, string>
     */
    public function sourceCatalog(): array
    {
        $catalog = [
            'field'     => __('Help4 field', 'help4-builder-suite'),
            'post'      => __('Post or page', 'help4-builder-suite'),
            'post_meta' => __('Approved post metadata', 'help4-builder-suite'),
            'product'   => __('Help4 product', 'help4-builder-suite'),
            'product_field' => __('Help4 product field', 'help4-builder-suite'),
            'variant'   => __('Help4 product variant', 'help4-builder-suite'),
            'term'      => __('Taxonomy term', 'help4-builder-suite'),
            'user'      => __('Author or user', 'help4-builder-suite'),
            'site'      => __('Site information', 'help4-builder-suite'),
            'option'    => __('Approved site option', 'help4-builder-suite'),
            'request'   => __('Approved request value', 'help4-builder-suite'),
        ];

        $filtered = apply_filters('h4bs_dynamic_data_sources', $catalog);
        return is_array($filtered) ? $filtered : $catalog;
    }

    /**
     * @param array<string, mixed> $query
     */
    public function resolve(array $query): mixed
    {
        $source = sanitize_key((string) ($query['source'] ?? 'field'));
        if (! array_key_exists($source, $this->sourceCatalog())) {
            return null;
        }

        $value = match ($source) {
            'field'     => $this->resolveHelp4Field($query),
            'post'      => $this->resolvePost($query),
            'post_meta' => $this->resolvePostMeta($query),
            'product'   => $this->resolveProduct($query),
            'product_field' => $this->resolveProductField($query),
            'variant'   => $this->resolveVariant($query),
            'term'      => $this->resolveTerm($query),
            'user'      => $this->resolveUser($query),
            'site'      => $this->resolveSite($query),
            'option'    => $this->resolveOption($query),
            'request'   => $this->resolveRequest($query),
            default     => null,
        };

        $path = $this->cleanPath((string) ($query['path'] ?? ''));
        if ($path !== '') {
            $value = $this->valueAtPath($value, $path);
        }

        $value = $this->applyTransforms($value, (string) ($query['transform'] ?? ''), $query);
        if ($this->isEmpty($value) && array_key_exists('fallback', $query)) {
            $value = $query['fallback'];
        }

        return apply_filters('h4bs_dynamic_data_value', $value, $source, $query);
    }

    /**
     * @param array<string, mixed> $query
     */
    private function resolveHelp4Field(array $query): mixed
    {
        $key = sanitize_key((string) ($query['key'] ?? ''));
        $post = $this->readablePost($this->postId($query));
        if ($key === '' || ! $post instanceof WP_Post) {
            return null;
        }

        return $this->decodeJson(get_post_meta($post->ID, '_h4f_' . $key, true));
    }

    /**
     * @param array<string, mixed> $query
     */
    private function resolvePost(array $query): mixed
    {
        $post = $this->readablePost($this->postId($query));
        if (! $post instanceof WP_Post) {
            return null;
        }

        $featuredId = (int) get_post_thumbnail_id($post);
        $data = [
            'id'                 => (int) $post->ID,
            'title'              => get_the_title($post),
            'content'            => (string) $post->post_content,
            'excerpt'            => has_excerpt($post) ? (string) $post->post_excerpt : wp_trim_words(wp_strip_all_tags((string) $post->post_content), 55),
            'slug'               => (string) $post->post_name,
            'status'             => (string) $post->post_status,
            'type'               => (string) $post->post_type,
            'date'               => (string) $post->post_date,
            'date_gmt'           => (string) $post->post_date_gmt,
            'modified'           => (string) $post->post_modified,
            'modified_gmt'       => (string) $post->post_modified_gmt,
            'author_id'          => (int) $post->post_author,
            'parent_id'          => (int) $post->post_parent,
            'menu_order'         => (int) $post->menu_order,
            'comment_count'      => (int) $post->comment_count,
            'permalink'          => (string) get_permalink($post),
            'featured_image_id'  => $featuredId,
            'featured_image_url' => $featuredId > 0 ? (string) wp_get_attachment_image_url($featuredId, 'full') : '',
        ];

        $key = sanitize_key((string) ($query['key'] ?? ''));
        return $key === '' ? $data : ($data[$key] ?? null);
    }

    /**
     * @param array<string, mixed> $query
     */
    private function resolvePostMeta(array $query): mixed
    {
        $post = $this->readablePost($this->postId($query));
        $key = sanitize_key((string) ($query['key'] ?? ''));
        if (! $post instanceof WP_Post || $key === '' || ! $this->postMetaKeyAllowed($key, $post->ID)) {
            return null;
        }

        return $this->decodeJson(get_post_meta($post->ID, $key, true));
    }

    /**
     * @param array<string, mixed> $query
     */
    private function resolveProduct(array $query): mixed
    {
        $productId = absint($query['object_id'] ?? 0);
        if ($productId <= 0) {
            $productId = absint($query['product_id'] ?? 0);
        }
        if ($productId <= 0) {
            $productId = absint(get_query_var('h4_product_id'));
        }
        if ($productId <= 0) {
            $productId = absint(apply_filters('h4bs_dynamic_current_product_id', 0, $query));
        }
        if ($productId <= 0) {
            return null;
        }

        $product = $this->products->get($productId);
        if (! is_array($product)) {
            return null;
        }
        if ((string) ($product['status'] ?? '') !== 'active' && ! current_user_can('manage_options')) {
            return null;
        }

        $payload = $this->publicProductPayload($this->decodeArray($product['payload'] ?? []));
        unset($product['payload']);
        $product['id'] = (int) ($product['id'] ?? $productId);
        $product['price'] = (float) ($product['price'] ?? 0);
        $product['stock_qty'] = (int) ($product['stock_qty'] ?? 0);
        $product['payload'] = $payload;
        $product['permalink'] = (string) apply_filters('h4bs_dynamic_product_permalink', '', $product, $query);

        $key = sanitize_key((string) ($query['key'] ?? ''));
        if ($key === '') {
            return $product;
        }
        if (array_key_exists($key, $product)) {
            return $product[$key];
        }
        return $payload[$key] ?? null;
    }

    /**
     * @param array<string, mixed> $query
     */
    private function resolveProductField(array $query): mixed
    {
        $productId = absint($query['object_id'] ?? 0);
        if ($productId <= 0) {
            $productId = absint($query['product_id'] ?? 0);
        }
        $key = sanitize_key((string) ($query['key'] ?? ''));
        if ($productId <= 0 || $key === '') {
            return null;
        }
        $product = $this->products->get($productId);
        if (! is_array($product) || ((string) ($product['status'] ?? '') !== 'active' && ! current_user_can('manage_options'))) {
            return null;
        }
        $payload = $this->decodeArray($product['payload'] ?? []);
        $fields = is_array($payload['_h4_fields'] ?? null) ? $payload['_h4_fields'] : [];
        return array_key_exists($key, $fields) ? $this->decodeJson($fields[$key]) : null;
    }

    /**
     * @param array<string, mixed> $query
     */
    private function resolveVariant(array $query): mixed
    {
        $productId = absint($query['product_id'] ?? ($query['object_id'] ?? 0));
        $variantId = absint($query['variant_id'] ?? 0);
        $sku = sanitize_text_field((string) ($query['sku'] ?? ''));
        if ($productId <= 0) {
            return null;
        }

        $variant = $sku !== '' ? $this->products->getVariantBySku($productId, $sku) : null;
        if (! is_array($variant) && $variantId > 0) {
            foreach ($this->products->listVariants($productId) as $candidate) {
                if ((int) ($candidate['id'] ?? 0) === $variantId) {
                    $variant = $candidate;
                    break;
                }
            }
        }
        if (! is_array($variant)) {
            return null;
        }

        $variant['id'] = (int) ($variant['id'] ?? 0);
        $variant['product_id'] = (int) ($variant['product_id'] ?? $productId);
        $variant['price'] = (float) ($variant['price'] ?? 0);
        $variant['stock_qty'] = (int) ($variant['stock_qty'] ?? 0);
        $variant['image_id'] = (int) ($variant['image_id'] ?? 0);
        $variant['image_url'] = $variant['image_id'] > 0 ? (string) wp_get_attachment_image_url($variant['image_id'], 'full') : '';
        $variant['attributes'] = $this->decodeArray($variant['attributes_json'] ?? []);
        unset($variant['attributes_json']);

        $key = sanitize_key((string) ($query['key'] ?? ''));
        return $key === '' ? $variant : ($variant[$key] ?? null);
    }

    /**
     * @param array<string, mixed> $query
     */
    private function resolveTerm(array $query): mixed
    {
        $termId = absint($query['object_id'] ?? 0);
        if ($termId <= 0) {
            $termId = absint($query['term_id'] ?? 0);
        }
        $taxonomy = sanitize_key((string) ($query['taxonomy'] ?? ''));
        $term = null;
        if ($termId > 0) {
            $term = $taxonomy !== '' ? get_term($termId, $taxonomy) : get_term($termId);
        } else {
            $queried = get_queried_object();
            if ($queried instanceof WP_Term) {
                $term = $queried;
            } elseif ($taxonomy !== '') {
                $post = $this->readablePost($this->postId($query));
                $terms = $post instanceof WP_Post ? get_the_terms($post, $taxonomy) : false;
                if (is_array($terms) && isset($terms[0]) && $terms[0] instanceof WP_Term) {
                    $term = $terms[0];
                }
            }
        }
        if (! $term instanceof WP_Term) {
            return null;
        }

        $link = get_term_link($term);
        $data = [
            'id'          => (int) $term->term_id,
            'name'        => (string) $term->name,
            'slug'        => (string) $term->slug,
            'description' => (string) $term->description,
            'taxonomy'    => (string) $term->taxonomy,
            'parent_id'   => (int) $term->parent,
            'count'       => (int) $term->count,
            'permalink'   => is_wp_error($link) ? '' : (string) $link,
        ];
        $key = sanitize_key((string) ($query['key'] ?? ''));
        return $key === '' ? $data : ($data[$key] ?? null);
    }

    /**
     * @param array<string, mixed> $query
     */
    private function resolveUser(array $query): mixed
    {
        $userId = absint($query['object_id'] ?? 0);
        if ($userId <= 0) {
            $userId = absint($query['user_id'] ?? 0);
        }
        if ($userId <= 0) {
            $post = $this->readablePost($this->postId($query));
            $userId = $post instanceof WP_Post ? (int) $post->post_author : get_current_user_id();
        }
        $user = get_userdata($userId);
        if (! $user instanceof WP_User) {
            return null;
        }

        $data = [
            'id'           => (int) $user->ID,
            'display_name' => (string) $user->display_name,
            'first_name'   => (string) $user->first_name,
            'last_name'    => (string) $user->last_name,
            'description'  => (string) get_user_meta($user->ID, 'description', true),
            'url'          => (string) $user->user_url,
            'posts_url'    => (string) get_author_posts_url($user->ID),
            'avatar_url'   => (string) get_avatar_url($user->ID),
        ];
        $filtered = apply_filters('h4bs_dynamic_data_user', $data, $user, $query);
        if (is_array($filtered)) {
            $data = $filtered;
        }

        $key = sanitize_key((string) ($query['key'] ?? ''));
        return $key === '' ? $data : ($data[$key] ?? null);
    }

    /**
     * @param array<string, mixed> $query
     */
    private function resolveSite(array $query): mixed
    {
        $data = [
            'name'        => (string) get_bloginfo('name'),
            'description' => (string) get_bloginfo('description'),
            'url'         => (string) home_url('/'),
            'admin_url'   => current_user_can('edit_posts') ? (string) admin_url() : '',
            'language'    => (string) get_bloginfo('language'),
            'charset'     => (string) get_bloginfo('charset'),
            'year'        => (string) wp_date('Y'),
            'timezone'    => (string) wp_timezone_string(),
        ];
        $key = sanitize_key((string) ($query['key'] ?? ''));
        return $key === '' ? $data : ($data[$key] ?? null);
    }

    /**
     * @param array<string, mixed> $query
     */
    private function resolveOption(array $query): mixed
    {
        $key = sanitize_key((string) ($query['key'] ?? ''));
        $allowed = [
            'blogname',
            'blogdescription',
            'date_format',
            'time_format',
            'timezone_string',
            'start_of_week',
            'page_on_front',
            'page_for_posts',
            'permalink_structure',
            'wplang',
        ];
        $allowed = apply_filters('h4bs_dynamic_data_allowed_options', $allowed, $query);
        if ($key === '' || ! is_array($allowed) || ! in_array($key, array_map('sanitize_key', $allowed), true)) {
            return null;
        }
        $optionKey = $key === 'wplang' ? 'WPLANG' : $key;
        return $this->decodeJson(get_option($optionKey));
    }

    /**
     * @param array<string, mixed> $query
     */
    private function resolveRequest(array $query): mixed
    {
        $key = sanitize_key((string) ($query['key'] ?? ''));
        $allowed = apply_filters('h4bs_dynamic_data_allowed_request_keys', ['s', 'paged', 'page', 'post_type', 'taxonomy', 'term'], $query);
        if ($key === '' || ! is_array($allowed) || ! in_array($key, array_map('sanitize_key', $allowed), true)) {
            return null;
        }
        $value = get_query_var($key, null);
        if ($value === null) {
            $filteredInput = filter_input(INPUT_GET, $key, FILTER_UNSAFE_RAW, FILTER_REQUIRE_SCALAR);
            if (is_string($filteredInput)) {
                $value = $filteredInput;
            }
        }
        return is_array($value) ? map_deep($value, 'sanitize_text_field') : sanitize_text_field((string) $value);
    }

    /**
     * @param array<string, mixed> $query
     */
    private function postId(array $query): int
    {
        $postId = absint($query['object_id'] ?? ($query['post_id'] ?? 0));
        if ($postId <= 0) {
            $postId = (int) get_the_ID();
        }
        if ($postId <= 0) {
            $postId = (int) get_queried_object_id();
        }
        return $postId;
    }

    private function readablePost(int $postId): ?WP_Post
    {
        $post = $postId > 0 ? get_post($postId) : null;
        if (! $post instanceof WP_Post) {
            return null;
        }
        if ($post->post_status === 'publish' || current_user_can('read_post', $post->ID)) {
            return $post;
        }
        return null;
    }

    private function postMetaKeyAllowed(string $key, int $postId): bool
    {
        $allowed = str_starts_with($key, '_h4f_');
        return (bool) apply_filters('h4bs_dynamic_data_post_meta_allowed', $allowed, $key, $postId);
    }

    /**
     * @param array<string, mixed> $payload
     * @return array<string, mixed>
     */
    private function publicProductPayload(array $payload): array
    {
        $allowedKeys = [
            'type', 'product_type', 'description', 'short_description', 'summary', 'excerpt',
            'featured_image_id', 'image_id', 'thumbnail_id', 'featured_image_url', 'image_url',
            'thumbnail_url', 'gallery_image_ids', 'gallery_urls', 'categories', 'tags', 'brand',
            'attributes', 'options', 'pricing', 'inventory', 'merchandising', 'subscription',
            'external_url', 'product_url', 'permalink', 'url', 'weight', 'dimensions', 'tax_class',
        ];
        $allowedKeys = apply_filters('h4bs_dynamic_data_product_payload_keys', $allowedKeys, $payload);
        $allowedMap = array_fill_keys(array_map('sanitize_key', is_array($allowedKeys) ? $allowedKeys : []), true);
        $public = [];
        foreach ($payload as $key => $value) {
            $cleanKey = sanitize_key((string) $key);
            if ($cleanKey === '' || ! isset($allowedMap[$cleanKey])) {
                continue;
            }
            $public[$cleanKey] = $this->sanitizePublicValue($value);
        }
        $filtered = apply_filters('h4bs_dynamic_data_product_payload', $public, $payload);
        return is_array($filtered) ? $filtered : $public;
    }

    private function sanitizePublicValue(mixed $value, int $depth = 0): mixed
    {
        if ($depth >= self::MAX_PATH_DEPTH) {
            return null;
        }
        if (is_array($value)) {
            $clean = [];
            foreach (array_slice($value, 0, 250, true) as $key => $item) {
                $cleanKey = is_int($key) ? $key : sanitize_key((string) $key);
                if ($cleanKey === '') {
                    continue;
                }
                if (is_string($cleanKey) && preg_match('/(?:secret|token|password|passwd|auth|credential|private_key|webhook)/i', $cleanKey) === 1) {
                    continue;
                }
                $clean[$cleanKey] = $this->sanitizePublicValue($item, $depth + 1);
            }
            return $clean;
        }
        if (is_bool($value) || is_int($value) || is_float($value) || $value === null) {
            return $value;
        }
        return wp_kses_post((string) $value);
    }

    private function decodeJson(mixed $value): mixed
    {
        if (! is_string($value)) {
            return $value;
        }
        $trimmed = trim($value);
        if ($trimmed === '' || ! in_array($trimmed[0], ['{', '['], true)) {
            return $value;
        }
        $decoded = json_decode($trimmed, true);
        return json_last_error() === JSON_ERROR_NONE ? $decoded : $value;
    }

    /**
     * @return array<string, mixed>
     */
    private function decodeArray(mixed $value): array
    {
        $decoded = $this->decodeJson($value);
        return is_array($decoded) ? $decoded : [];
    }

    private function cleanPath(string $path): string
    {
        $segments = array_slice(array_filter(array_map('trim', explode('.', $path))), 0, self::MAX_PATH_DEPTH);
        $clean = [];
        foreach ($segments as $segment) {
            $segment = preg_replace('/[^A-Za-z0-9_-]/', '', $segment) ?: '';
            if ($segment !== '') {
                $clean[] = $segment;
            }
        }
        return implode('.', $clean);
    }

    private function valueAtPath(mixed $value, string $path): mixed
    {
        $current = $value;
        foreach (explode('.', $path) as $segment) {
            if (is_object($current)) {
                $current = get_object_vars($current);
            }
            if (! is_array($current)) {
                return null;
            }
            $key = ctype_digit($segment) ? (int) $segment : $segment;
            if (! array_key_exists($key, $current)) {
                return null;
            }
            $current = $current[$key];
        }
        return $current;
    }

    /**
     * @param array<string, mixed> $query
     */
    private function applyTransforms(mixed $value, string $pipeline, array $query): mixed
    {
        $transforms = array_slice(array_filter(array_map('trim', explode('|', $pipeline))), 0, self::MAX_TRANSFORMS);
        foreach ($transforms as $transform) {
            [$name, $argument] = array_pad(explode(':', $transform, 2), 2, '');
            $name = sanitize_key($name);
            $argument = rawurldecode($argument);
            $value = match ($name) {
                'trim'       => is_scalar($value) ? trim((string) $value) : $value,
                'strip_tags' => is_scalar($value) ? wp_strip_all_tags((string) $value) : $value,
                'lower'      => is_scalar($value) ? mb_strtolower((string) $value) : $value,
                'upper'      => is_scalar($value) ? mb_strtoupper((string) $value) : $value,
                'title'      => is_scalar($value) ? mb_convert_case((string) $value, MB_CASE_TITLE) : $value,
                'words'      => is_scalar($value) ? wp_trim_words((string) $value, max(1, min(500, (int) $argument))) : $value,
                'chars'      => is_scalar($value) ? mb_substr((string) $value, 0, max(1, min(5000, (int) $argument))) : $value,
                'count'      => is_countable($value) ? count($value) : ($this->isEmpty($value) ? 0 : 1),
                'first'      => is_array($value) ? reset($value) : $value,
                'last'       => is_array($value) ? end($value) : $value,
                'join'       => is_array($value) ? implode($argument !== '' ? $argument : ', ', array_map([$this, 'scalarString'], $value)) : $value,
                'number'     => is_numeric($value) ? number_format_i18n((float) $value, max(0, min(6, (int) ($argument !== '' ? $argument : 0)))) : $value,
                'currency'   => $this->currency($value, $argument !== '' ? $argument : (string) ($query['currency'] ?? 'USD')),
                'date'       => $this->dateValue($value, $argument !== '' ? $argument : (string) get_option('date_format')),
                'url'        => is_scalar($value) ? esc_url_raw((string) $value) : $value,
                'json'       => wp_json_encode($value),
                default      => apply_filters('h4bs_dynamic_data_transform', $value, $name, $argument, $query),
            };
        }
        return $value;
    }

    private function scalarString(mixed $value): string
    {
        return is_scalar($value) ? (string) $value : (string) wp_json_encode($value);
    }

    private function currency(mixed $value, string $currency): mixed
    {
        if (! is_numeric($value)) {
            return $value;
        }
        $currency = strtoupper(substr(preg_replace('/[^A-Z]/', '', strtoupper($currency)) ?: 'USD', 0, 8));
        return $currency . ' ' . number_format_i18n((float) $value, 2);
    }

    private function dateValue(mixed $value, string $format): mixed
    {
        if (! is_scalar($value) || trim((string) $value) === '') {
            return $value;
        }
        $timestamp = is_numeric($value) ? (int) $value : strtotime((string) $value);
        if ($timestamp === false || $timestamp <= 0) {
            return $value;
        }
        $format = substr(wp_strip_all_tags($format), 0, 80);
        return wp_date($format !== '' ? $format : 'Y-m-d', $timestamp);
    }

    private function isEmpty(mixed $value): bool
    {
        return $value === null || $value === '' || $value === [];
    }
}
