<?php

/**
 * Condition Evaluator Service
 *
 * Core service that evaluates restriction conditions against a user.
 * Supports subscription status, purchase history, feature values,
 * and WordPress roles with AND/OR logic.
 *
 * @package ArraySubs\Features\MembersAccess\Services
 * @since 1.0.0
 */

namespace ArraySubs\Features\MembersAccess\Services;

defined('ABSPATH') || exit;

/**
 * ConditionEvaluator class
 */
class ConditionEvaluator
{
    /**
     * Cache for evaluation results
     *
     * @var array
     */
    private static array $cache = [];

    /**
     * Purchase history service
     *
     * @var PurchaseHistory
     */
    private PurchaseHistory $purchase_history;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->purchase_history = new PurchaseHistory();
    }

    /**
     * Evaluate conditions for a user
     *
     * @param int   $user_id    User ID (0 for guest).
     * @param array $conditions Conditions array with 'logic' and 'rules'.
     * @return bool True if user meets conditions.
     */
    public function evaluate(int $user_id, array $conditions): bool
    {
        if (empty($conditions['rules'])) {
            return true; // No conditions = access granted
        }

        // Generate cache key
        $cache_key = $this->getCacheKey($user_id, $conditions);
        if (isset(self::$cache[$cache_key])) {
            return self::$cache[$cache_key];
        }

        $logic = $conditions['logic'] ?? 'and';
        $rules = $conditions['rules'];

        $results = [];
        foreach ($rules as $rule) {
            $result = $this->evaluateRule($user_id, $rule);
            $results[] = $result;

            // Short-circuit evaluation
            if ($logic === 'and' && !$result) {
                self::$cache[$cache_key] = false;
                return false;
            }
            if ($logic === 'or' && $result) {
                self::$cache[$cache_key] = true;
                return true;
            }
        }

        // Final evaluation
        $final_result = ($logic === 'and') ? !in_array(false, $results, true) : in_array(true, $results, true);
        self::$cache[$cache_key] = $final_result;

        $this->debugLog($user_id, $conditions, $final_result);

        return $final_result;
    }

    /**
     * Evaluate a single rule
     *
     * @param int   $user_id User ID.
     * @param array $rule    Rule configuration.
     * @return bool
     */
    private function evaluateRule(int $user_id, array $rule): bool
    {
        $type = $rule['type'] ?? '';

        switch ($type) {
            case 'group':
                // Nested group - recursive evaluation
                return $this->evaluate($user_id, $rule);

            case 'subscription_status':
                return $this->evaluateSubscriptionStatus($user_id, $rule);

            case 'has_active_subscription':
                return $this->evaluateHasActiveSubscription($user_id, $rule);

            case 'has_subscription_variation':
                return $this->evaluateHasSubscriptionVariation($user_id, $rule);

            case 'lifetime_spent':
                return $this->evaluateLifetimeSpent($user_id, $rule);

            case 'purchased_product':
                return $this->evaluatePurchasedProduct($user_id, $rule);

            case 'purchased_variation':
                return $this->evaluatePurchasedVariation($user_id, $rule);

            case 'purchased_from_group':
                return $this->evaluatePurchasedFromGroup($user_id, $rule);

            case 'feature_value':
                return $this->evaluateFeatureValue($user_id, $rule);

            case 'user_role':
                return $this->evaluateUserRole($user_id, $rule);

            default:
                // Unknown rule type - log and skip
                $this->debugLog($user_id, ['unknown_rule' => $type], false);
                return false;
        }
    }

    /**
     * Evaluate subscription status condition
     *
     * @param int   $user_id User ID.
     * @param array $rule    Rule with 'statuses' key.
     * @return bool
     */
    private function evaluateSubscriptionStatus(int $user_id, array $rule): bool
    {
        if ($user_id === 0) {
            return false;
        }

        $required_statuses = $rule['statuses'] ?? ['active'];

        // Map simple status names to full status names
        $status_map = [
            'active'    => 'arraysubs-active',
            'trial'     => 'arraysubs-trial',
            'on-hold'   => 'arraysubs-on-hold',
            'cancelled' => 'arraysubs-cancelled',
            'expired'   => 'arraysubs-expired',
            'pending'   => 'arraysubs-pending',
        ];

        $full_statuses = [];
        foreach ($required_statuses as $status) {
            $full_statuses[] = $status_map[$status] ?? $status;
        }

        // Get user subscriptions with required statuses
        $subscriptions = arraysubs_get_customer_subscriptions($user_id, $full_statuses);

        return !empty($subscriptions);
    }

    /**
     * Evaluate has active subscription condition
     *
     * @param int   $user_id User ID.
     * @param array $rule    Rule with optional 'product_ids' key.
     * @return bool
     */
    private function evaluateHasActiveSubscription(int $user_id, array $rule): bool
    {
        if ($user_id === 0) {
            return false;
        }

        $product_ids = $rule['product_ids'] ?? [];

        // Get active subscriptions
        $subscriptions = arraysubs_get_customer_subscriptions(
            $user_id,
            ['arraysubs-active', 'arraysubs-trial']
        );

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

        // If no specific products required, any active subscription is enough
        if (empty($product_ids)) {
            return true;
        }

        // Check if any subscription is for the specified products
        foreach ($subscriptions as $subscription) {
            $subscription_id = arraysubs_get_normalized_subscription_id($subscription);

            if ($subscription_id <= 0) {
                continue;
            }

            $sub_product_id = get_post_meta($subscription_id, '_product_id', true);
            if (in_array((int) $sub_product_id, $product_ids, true)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Evaluate has subscription variation condition
     *
     * @param int   $user_id User ID.
     * @param array $rule    Rule with 'variation_ids' key.
     * @return bool
     */
    private function evaluateHasSubscriptionVariation(int $user_id, array $rule): bool
    {
        if ($user_id === 0) {
            return false;
        }

        $variation_ids = $rule['variation_ids'] ?? [];
        if (empty($variation_ids)) {
            return false;
        }

        // Get active subscriptions
        $subscriptions = arraysubs_get_customer_subscriptions(
            $user_id,
            ['arraysubs-active', 'arraysubs-trial']
        );

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

        // Check if any subscription is for the specified variations
        foreach ($subscriptions as $subscription) {
            $subscription_id = arraysubs_get_normalized_subscription_id($subscription);

            if ($subscription_id <= 0) {
                continue;
            }

            $sub_variation_id = get_post_meta($subscription_id, '_variation_id', true);
            if ($sub_variation_id && in_array((int) $sub_variation_id, $variation_ids, true)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Evaluate lifetime spent condition
     *
     * @param int   $user_id User ID.
     * @param array $rule    Rule with 'operator' and 'amount' keys.
     * @return bool
     */
    private function evaluateLifetimeSpent(int $user_id, array $rule): bool
    {
        if ($user_id === 0) {
            return false;
        }

        $operator = $rule['operator'] ?? '>=';
        $amount = floatval($rule['amount'] ?? 0);

        $lifetime_spent = $this->purchase_history->getLifetimeSpent($user_id);

        return $this->compareValues($lifetime_spent, $operator, $amount);
    }

    /**
     * Evaluate purchased product condition
     *
     * @param int   $user_id User ID.
     * @param array $rule    Rule with 'product_ids' key.
     * @return bool
     */
    private function evaluatePurchasedProduct(int $user_id, array $rule): bool
    {
        if ($user_id === 0) {
            return false;
        }

        $product_ids = $rule['product_ids'] ?? [];
        if (empty($product_ids)) {
            return false;
        }

        // Check if user has purchased ANY of the specified products
        foreach ($product_ids as $product_id) {
            if ($this->purchase_history->hasPurchasedProduct($user_id, (int) $product_id)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Evaluate purchased variation condition
     *
     * @param int   $user_id User ID.
     * @param array $rule    Rule with 'variation_ids' key.
     * @return bool
     */
    private function evaluatePurchasedVariation(int $user_id, array $rule): bool
    {
        if ($user_id === 0) {
            return false;
        }

        $variation_ids = $rule['variation_ids'] ?? [];
        if (empty($variation_ids)) {
            return false;
        }

        // Check if user has purchased ANY of the specified variations
        foreach ($variation_ids as $variation_id) {
            if ($this->purchase_history->hasPurchasedVariation($user_id, (int) $variation_id)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Evaluate purchased from group (category/tag) condition
     *
     * @param int   $user_id User ID.
     * @param array $rule    Rule with 'taxonomy' and 'term_ids' keys.
     * @return bool
     */
    private function evaluatePurchasedFromGroup(int $user_id, array $rule): bool
    {
        if ($user_id === 0) {
            return false;
        }

        $taxonomy = $rule['taxonomy'] ?? '';
        $term_ids = $rule['term_ids'] ?? [];

        if (empty($taxonomy) || empty($term_ids)) {
            return false;
        }

        return $this->purchase_history->hasPurchasedFromGroup($user_id, $taxonomy, $term_ids);
    }

    /**
     * Evaluate feature value condition
     *
     * @param int   $user_id User ID.
     * @param array $rule    Rule with 'feature_key', 'operator', 'value', 'aggregation' keys.
     * @return bool
     */
    private function evaluateFeatureValue(int $user_id, array $rule): bool
    {
        if ($user_id === 0) {
            return false;
        }

        $feature_key = $rule['feature_key'] ?? '';
        $operator = $rule['operator'] ?? '>=';
        $required_value = $rule['value'] ?? 0;
        $aggregation = $rule['aggregation'] ?? 'sum';

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

        $user_feature_value = $this->getAggregatedFeatureValue($user_id, $feature_key, $aggregation);

        return $this->compareValues($user_feature_value, $operator, $required_value);
    }

    /**
     * Get aggregated feature value from user's purchased products
     *
     * @param int    $user_id     User ID.
     * @param string $feature_key Feature key.
     * @param string $aggregation Aggregation mode: sum, max, any.
     * @return mixed
     */
    public function getAggregatedFeatureValue(int $user_id, string $feature_key, string $aggregation = 'sum')
    {
        // Get all purchased product IDs
        $purchased_product_ids = $this->purchase_history->getPurchasedProductIds($user_id);

        // Also include products from active subscriptions
        $subscriptions = arraysubs_get_customer_subscriptions(
            $user_id,
            ['arraysubs-active', 'arraysubs-trial']
        );

        foreach ($subscriptions as $subscription) {
            $subscription_id = arraysubs_get_normalized_subscription_id($subscription);

            if ($subscription_id <= 0) {
                continue;
            }

            $product_id = get_post_meta($subscription_id, '_product_id', true);
            if ($product_id) {
                $purchased_product_ids[] = (int) $product_id;
            }
            $variation_id = get_post_meta($subscription_id, '_variation_id', true);
            if ($variation_id) {
                $purchased_product_ids[] = (int) $variation_id;
            }
        }

        $purchased_product_ids = array_unique($purchased_product_ids);

        if (empty($purchased_product_ids)) {
            return $aggregation === 'any' ? false : 0;
        }

        $values = [];

        foreach ($purchased_product_ids as $product_id) {
            $features = $this->getProductFeatures($product_id);
            if (isset($features[$feature_key])) {
                $values[] = $this->normalizeFeatureValue($features[$feature_key]);
            }
        }

        if (empty($values)) {
            return $aggregation === 'any' ? false : 0;
        }

        switch ($aggregation) {
            case 'sum':
                return array_sum($values);

            case 'max':
                return max($values);

            case 'any':
                // Presence test: count only entitlements that are actually "on"
                // so an explicitly-off toggle ("No"/0) does not grant access.
                foreach ($values as $value) {
                    if ($this->isTruthyFeatureValue($value)) {
                        return true;
                    }
                }
                return false;

            default:
                return array_sum($values);
        }
    }

    /**
     * Normalize a stored feature value for aggregation.
     *
     * Maps the literal "Unlimited" to PHP_INT_MAX (so it satisfies any minimum
     * under sum/max) and coerces numeric strings to numbers (avoiding PHP 8
     * "Addition not supported on type string" warnings from array_sum).
     *
     * @param mixed $value Raw stored feature value.
     * @return mixed
     */
    private function normalizeFeatureValue($value)
    {
        if (is_string($value) && strcasecmp(trim($value), 'unlimited') === 0) {
            return PHP_INT_MAX;
        }

        if (is_numeric($value)) {
            return $value + 0;
        }

        return $value;
    }

    /**
     * Whether a (normalized) feature value represents an enabled/on entitlement.
     *
     * @param mixed $value Normalized feature value.
     * @return bool
     */
    private function isTruthyFeatureValue($value): bool
    {
        if (is_numeric($value)) {
            return (float) $value > 0;
        }

        return in_array(strtolower(trim((string) $value)), ['yes', 'true', '1', 'on'], true);
    }

    /**
     * Get features from a product
     *
     * Reads the _arraysubs_features meta from a product and returns
     * feature name => value pairs. Only enabled features are returned.
     *
     * @param int $product_id Product ID.
     * @return array Feature name => value pairs.
     */
    private function getProductFeatures(int $product_id): array
    {
        $features_json = get_post_meta($product_id, '_arraysubs_features', true);
        if (empty($features_json)) {
            return [];
        }

        $features = json_decode($features_json, true);
        if (!is_array($features)) {
            return [];
        }

        // Convert to name => value pairs (only enabled features)
        // Feature structure: { id, name, type, value, enabled, order }
        $result = [];
        foreach ($features as $feature) {
            // Check if feature is enabled and has required fields
            if (!empty($feature['enabled']) && isset($feature['name']) && isset($feature['value'])) {
                $result[$feature['name']] = $feature['value'];
            }
        }

        return $result;
    }

    /**
     * Evaluate user role condition
     *
     * @param int   $user_id User ID.
     * @param array $rule    Rule with 'roles' key.
     * @return bool
     */
    private function evaluateUserRole(int $user_id, array $rule): bool
    {
        if ($user_id === 0) {
            return false;
        }

        $required_roles = $rule['roles'] ?? [];
        if (empty($required_roles)) {
            return false;
        }

        $user = get_userdata($user_id);
        if (!$user) {
            return false;
        }

        $user_roles = $user->roles;

        // Check if user has ANY of the required roles
        $intersection = array_intersect($user_roles, $required_roles);
        return !empty($intersection);
    }

    /**
     * Compare values using operator
     *
     * @param mixed  $value    Value to check.
     * @param string $operator Comparison operator.
     * @param mixed  $target   Target value.
     * @return bool
     */
    private function compareValues($value, string $operator, $target): bool
    {
        switch ($operator) {
            case '=':
            case '==':
                if (is_numeric($value) && is_numeric($target)) {
                    return (float) $value === (float) $target;
                }
                return (string) $value === (string) $target;

            case '!=':
            case '<>':
                if (is_numeric($value) && is_numeric($target)) {
                    return (float) $value !== (float) $target;
                }
                return (string) $value !== (string) $target;

            case '>':
                return $value > $target;

            case '>=':
                return $value >= $target;

            case '<':
                return $value < $target;

            case '<=':
                return $value <= $target;

            case 'contains':
                return is_string($value) && strpos($value, $target) !== false;

            case 'not_contains':
                return is_string($value) && strpos($value, $target) === false;

            case 'in':
                return is_array($target) && in_array($value, $target, true);

            case 'not_in':
                return is_array($target) && !in_array($value, $target, true);

            case 'empty':
                return empty($value);

            case 'not_empty':
                return !empty($value);

            default:
                return false;
        }
    }

    /**
     * Generate cache key for conditions
     *
     * @param int   $user_id    User ID.
     * @param array $conditions Conditions array.
     * @return string
     */
    private function getCacheKey(int $user_id, array $conditions): string
    {
        return 'user_' . $user_id . '_' . md5(wp_json_encode($conditions));
    }

    /**
     * Debug log for condition evaluation
     *
     * @param int   $user_id    User ID.
     * @param array $conditions Conditions evaluated.
     * @param bool  $result     Evaluation result.
     * @return void
     */
    private function debugLog(int $user_id, array $conditions, bool $result): void
    {
        if (!defined('WP_DEBUG') || !WP_DEBUG) {
            return;
        }
    }

    /**
     * Clear evaluation cache
     *
     * @return void
     */
    public static function clearCache(): void
    {
        self::$cache = [];
    }
}
