<?php

/**
 * Schedule Evaluator Service
 *
 * Evaluates rule-level content dripping / scheduling.
 * When a rule has scheduling enabled, access is only granted after
 * the configured delay period has elapsed since the member's qualifying
 * subscription start date.
 *
 * @package ArraySubs\Features\MembersAccess\Services
 * @since 1.12.0
 */

namespace ArraySubs\Features\MembersAccess\Services;

defined('ABSPATH') || exit;

/**
 * ScheduleEvaluator class
 */
class ScheduleEvaluator
{

    /**
     * Request-level cache for schedule results.
     *
     * @var array
     */
    private static array $cache = [];

    /**
     * Default subscription statuses that can qualify for scheduled access.
     *
     * @var string[]
     */
    private const DEFAULT_QUALIFYING_STATUSES = array(
        'arraysubs-active',
        'arraysubs-trial',
    );

    /**
     * Explicit second values used for schedule delays.
     */
    private const SECONDS_IN_DAY  = 86400;
    private const SECONDS_IN_WEEK = 604800;

    /**
     * Check whether a rule's schedule has been satisfied for a user.
     *
     * Returns true when:
     * - The rule has no schedule (schedule_enabled is false/absent).
     * - The schedule delay has elapsed based on the member's earliest
     *   qualifying subscription start date.
     *
     * Returns false when:
     * - The schedule is enabled but the delay has not yet elapsed.
     * - The user has no qualifying subscriptions.
     *
     * @param int   $user_id User ID (0 for guests).
     * @param array $rule    The full rule array (must include schedule_* fields).
     * @return bool True if the schedule is satisfied (access allowed).
     */
    public function isScheduleSatisfied(int $user_id, array $rule): bool
    {
        if (! $this->isScheduleEnabled($rule)) {
            return true;
        }

        if (0 === $user_id) {
            return false;
        }

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

        $delay_seconds = $this->getDelayInSeconds($rule);
        if (0 === $delay_seconds) {
            self::$cache[$cache_key] = true;
            return true;
        }

        $earliest_start = $this->getEarliestQualifyingStartDate($user_id, $rule);
        if (null === $earliest_start) {
            self::$cache[$cache_key] = false;
            return false;
        }

        $unlock_timestamp = $earliest_start + $delay_seconds;
        $now              = (new \DateTimeImmutable('now', new \DateTimeZone('UTC')))->getTimestamp();
        $result           = $now >= $unlock_timestamp;

        self::$cache[$cache_key] = $result;
        return $result;
    }

    /**
     * Get the unlock date (UTC timestamp) for a user and rule.
     *
     * Useful for displaying "Available on {date}" messages.
     *
     * @param int   $user_id User ID.
     * @param array $rule    The rule array.
     * @return int|null Unix timestamp when the rule unlocks, or null if no qualifying subscription.
     */
    public function getUnlockTimestamp(int $user_id, array $rule): ?int
    {
        if (! $this->isScheduleEnabled($rule)) {
            return null;
        }

        if (0 === $user_id) {
            return null;
        }

        $delay_seconds  = $this->getDelayInSeconds($rule);
        $earliest_start = $this->getEarliestQualifyingStartDate($user_id, $rule);

        if (null === $earliest_start) {
            return null;
        }

        return $earliest_start + $delay_seconds;
    }

    /**
     * Check whether a rule has scheduling enabled.
     *
     * @param array $rule Rule array.
     * @return bool
     */
    public function isScheduleEnabled(array $rule): bool
    {
        return ! empty($rule['schedule_enabled']);
    }

    /**
     * Convert the rule's schedule configuration into seconds.
     *
     * @param array $rule Rule array with schedule_value and schedule_unit.
     * @return int Delay in seconds.
     */
    private function getDelayInSeconds(array $rule): int
    {
        $value = max(0, (int) ($rule['schedule_value'] ?? 0));
        $unit  = $rule['schedule_unit'] ?? 'days';

        switch ($unit) {
            case 'weeks':
                return $value * self::SECONDS_IN_WEEK;

            case 'months':
                return $value * 30 * self::SECONDS_IN_DAY;

            default: // days
                return $value * self::SECONDS_IN_DAY;
        }
    }

    /**
     * Find the earliest qualifying subscription start date for a user.
     *
     * Considers active and trial subscriptions. The method evaluates the
     * rule's conditions against each subscription to determine which ones
     * are "qualifying". Among those, the earliest start date wins so the
     * user gets the most generous timing.
     *
     * @param int   $user_id User ID.
     * @param array $rule    The rule array (contains conditions).
     * @return int|null Earliest start date as Unix timestamp, or null.
     */
    private function getEarliestQualifyingStartDate(int $user_id, array $rule): ?int
    {
        $conditions = $this->normalizeConditions($rule['conditions'] ?? array());

        $subscriptions = arraysubs_get_customer_subscriptions(
            $user_id,
            $this->getCandidateStatuses($conditions)
        );

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

        $earliest = null;

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

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

            if (! $this->subscriptionMatchesConditions($user_id, $subscription_id, $conditions)) {
                continue;
            }

            $start_date = get_post_meta($subscription_id, '_start_date', true);
            if (empty($start_date)) {
                continue;
            }

            $start_timestamp = arraysubs_get_datetime_timestamp_from_string(
                (string) $start_date,
                new \DateTimeZone('UTC')
            );

            if (null === $start_timestamp) {
                continue;
            }

            if (null === $earliest || $start_timestamp < $earliest) {
                $earliest = $start_timestamp;
            }
        }

        return $earliest;
    }

    /**
     * Normalize a rule conditions array.
     *
     * @param mixed $conditions Rule conditions.
     * @return array
     */
    private function normalizeConditions($conditions): array
    {
        if (! is_array($conditions)) {
            return array(
                'logic' => 'and',
                'rules' => array(),
            );
        }

        return array(
            'logic' => ('or' === ($conditions['logic'] ?? 'and')) ? 'or' : 'and',
            'rules' => (isset($conditions['rules']) && is_array($conditions['rules'])) ? $conditions['rules'] : array(),
        );
    }

    /**
     * Get candidate subscription statuses for schedule evaluation.
     *
     * @param array $conditions Normalized conditions array.
     * @return string[]
     */
    private function getCandidateStatuses(array $conditions): array
    {
        $statuses = $this->extractSubscriptionStatuses($conditions);

        if (empty($statuses)) {
            return self::DEFAULT_QUALIFYING_STATUSES;
        }

        return array_values(array_unique($statuses));
    }

    /**
     * Extract explicit subscription statuses referenced by conditions.
     *
     * @param array $conditions Normalized conditions array.
     * @return string[]
     */
    private function extractSubscriptionStatuses(array $conditions): array
    {
        $statuses = array();

        foreach ($conditions['rules'] as $rule) {
            if (! is_array($rule)) {
                continue;
            }

            if ('group' === ($rule['type'] ?? '')) {
                $statuses = array_merge(
                    $statuses,
                    $this->extractSubscriptionStatuses($this->normalizeConditions($rule))
                );
                continue;
            }

            if ('subscription_status' !== ($rule['type'] ?? '')) {
                continue;
            }

            foreach ((array) ($rule['statuses'] ?? array()) as $status) {
                $normalized_status = $this->normalizeSubscriptionStatus((string) $status);

                if (! empty($normalized_status)) {
                    $statuses[] = $normalized_status;
                }
            }
        }

        return $statuses;
    }

    /**
     * Check whether a specific subscription satisfies the rule conditions.
     *
     * @param int   $user_id         User ID.
     * @param int   $subscription_id Subscription ID.
     * @param array $conditions      Normalized conditions array.
     * @return bool
     */
    private function subscriptionMatchesConditions(int $user_id, int $subscription_id, array $conditions): bool
    {
        if (empty($conditions['rules'])) {
            return true;
        }

        $logic   = $conditions['logic'] ?? 'and';
        $results = array();

        foreach ($conditions['rules'] as $rule) {
            if (! is_array($rule)) {
                $results[] = false;
                continue;
            }

            $result    = $this->evaluateConditionRuleForSubscription($user_id, $subscription_id, $rule);
            $results[] = $result;

            if ('and' === $logic && ! $result) {
                return false;
            }

            if ('or' === $logic && $result) {
                return true;
            }
        }

        return ('or' === $logic)
            ? in_array(true, $results, true)
            : ! in_array(false, $results, true);
    }

    /**
     * Evaluate one rule against a specific subscription context.
     *
     * @param int   $user_id         User ID.
     * @param int   $subscription_id Subscription ID.
     * @param array $rule            Condition rule.
     * @return bool
     */
    private function evaluateConditionRuleForSubscription(int $user_id, int $subscription_id, array $rule): bool
    {
        $type = $rule['type'] ?? '';

        switch ($type) {
            case 'group':
                return $this->subscriptionMatchesConditions($user_id, $subscription_id, $this->normalizeConditions($rule));

            case 'subscription_status':
                $required_statuses = array_map(
                    array($this, 'normalizeSubscriptionStatus'),
                    (array) ($rule['statuses'] ?? array())
                );
                $required_statuses = array_values(array_filter($required_statuses));

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

                return in_array(get_post_status($subscription_id), $required_statuses, true);

            case 'has_active_subscription':
                $product_ids = array_map('absint', (array) ($rule['product_ids'] ?? array()));

                if (empty($product_ids)) {
                    return in_array(get_post_status($subscription_id), self::DEFAULT_QUALIFYING_STATUSES, true);
                }

                $effective_product_id = arraysubs_get_subscription_effective_product_id($subscription_id);
                $parent_product_id    = absint(get_post_meta($subscription_id, '_product_id', true));

                return in_array($effective_product_id, $product_ids, true)
                    || in_array($parent_product_id, $product_ids, true);

            case 'has_subscription_variation':
                $variation_ids = array_map('absint', (array) ($rule['variation_ids'] ?? array()));

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

                return in_array(
                    absint(get_post_meta($subscription_id, '_variation_id', true)),
                    $variation_ids,
                    true
                );

            default:
                $evaluator = new ConditionEvaluator();

                return $evaluator->evaluate(
                    $user_id,
                    array(
                        'logic' => 'and',
                        'rules' => array($rule),
                    )
                );
        }
    }

    /**
     * Normalize a subscription status label to the stored post status value.
     *
     * @param string $status Raw status label.
     * @return string
     */
    private function normalizeSubscriptionStatus(string $status): string
    {
        $status_map = array(
            'active'    => 'arraysubs-active',
            'trial'     => 'arraysubs-trial',
            'on-hold'   => 'arraysubs-on-hold',
            'cancelled' => 'arraysubs-cancelled',
            'expired'   => 'arraysubs-expired',
            'pending'   => 'arraysubs-pending',
        );

        $status = sanitize_text_field($status);

        return $status_map[$status] ?? $status;
    }

    /**
     * Generate a cache key for the schedule evaluation.
     *
     * @param int   $user_id User ID.
     * @param array $rule    Rule array.
     * @return string
     */
    private function getCacheKey(int $user_id, array $rule): string
    {
        $rule_id = $rule['id'] ?? '';
        $value   = $rule['schedule_value'] ?? 0;
        $unit    = $rule['schedule_unit'] ?? 'days';

        return 'schedule_' . $user_id . '_' . $rule_id . '_' . $value . '_' . $unit;
    }

    /**
     * Clear the request-level cache.
     *
     * @return void
     */
    public static function clearCache(): void
    {
        self::$cache = array();
    }
}
