<?php

/**
 * Role Manager Service
 *
 * Handles automatic WordPress role assignment based on subscription events.
 * Maps subscription products to WordPress roles for third-party plugin compatibility.
 *
 * @package ArraySubs\Features\MembersAccess\Services
 * @since 1.0.0
 */

namespace ArraySubs\Features\MembersAccess\Services;

defined('ABSPATH') || exit;

/**
 * RoleManager class
 */
class RoleManager
{
    /**
     * Handle subscription status change
     *
     * @param int    $subscription_id Subscription ID.
     * @param string $new_status      New status.
     * @param string $old_status      Old status.
     * @return void
     */
    public function handleStatusChange(int $subscription_id, string $new_status, string $old_status): void
    {
        $subscription = arraysubs_get_subscription($subscription_id);
        if (!$subscription instanceof \WP_Post) {
            return;
        }

        $customer_id = (int) get_post_meta($subscription_id, '_customer_id', true);
        if ($customer_id <= 0) {
            return;
        }

        // Get matching role mapping rules.
        $rules = $this->getMatchingRules($subscription);

        if (!$this->isActiveStatus($new_status)) {
            $rules = $this->mergeRules(
                $rules,
                $this->getRulesGrantingCurrentRoles($customer_id)
            );
        }

        foreach ($rules as $rule) {
            $this->applyRule($rule, $customer_id, $new_status, $old_status, $subscription_id);
        }

        $this->debugLog($subscription_id, $customer_id, $new_status, $old_status);
    }

    /**
     * Get role mapping rules that match this subscription
     *
     * @param mixed $subscription Subscription post object or subscription ID.
     * @return array
     */
    private function getMatchingRules($subscription): array
    {
        if (is_numeric($subscription)) {
            $subscription = arraysubs_get_subscription(absint($subscription));
        }

        if (!$subscription instanceof \WP_Post) {
            return [];
        }

        $rules = arraysubs_get_setting('members_access.role_mapping_rules', []);
        if (empty($rules)) {
            return [];
        }

        $product_id = (int) get_post_meta($subscription->ID, '_product_id', true);
        $variation_id = (int) get_post_meta($subscription->ID, '_variation_id', true);
        $customer_id = (int) get_post_meta($subscription->ID, '_customer_id', true);

        $matching_rules = [];

        foreach ($rules as $rule) {
            if (!isset($rule['enabled']) || !$rule['enabled']) {
                continue;
            }

            // Check if rule conditions match this subscription
            $conditions = $rule['conditions'] ?? [];
            if (empty($conditions['rules'])) {
                // No conditions = matches all subscriptions
                $matching_rules[] = $rule;
                continue;
            }

            // Evaluate conditions
            $evaluator = new ConditionEvaluator();
            if ($evaluator->evaluate($customer_id, $conditions)) {
                $matching_rules[] = $rule;
            }
        }

        return $matching_rules;
    }

    /**
     * Apply a role mapping rule to a user
     *
     * @param array  $rule       Role mapping rule.
     * @param int    $user_id    User ID.
     * @param string $new_status New subscription status.
     * @param string $old_status Old subscription status.
     * @return void
     */
    private function applyRule(array $rule, int $user_id, string $new_status, string $old_status, int $subscription_id = 0): void
    {
        $user = get_userdata($user_id);
        if (!$user) {
            return;
        }

        // Determine if subscription is now active or not
        $is_now_active = $this->isActiveStatus($new_status);
        $was_active = $this->isActiveStatus($old_status);

        // Check on-hold status
        $is_on_hold = $this->isOnHoldStatus($new_status);

        $add_roles = $rule['add_roles'] ?? [];
        $remove_roles = $rule['remove_roles'] ?? [];
        $on_hold_behavior = $rule['on_hold_behavior'] ?? 'keep';
        $fallback_role = $rule['fallback_role'] ?? '';

        // Subscription became active
        if ($is_now_active && !$was_active) {
            // Add configured roles
            foreach ($add_roles as $role) {
                $this->addUserRole($user, $role, $subscription_id);
            }
            // Remove roles that should be removed when active
            foreach ($remove_roles as $role) {
                $this->removeUserRole($user, $role, $subscription_id);
            }
        }

        // Subscription is on-hold
        if ($is_on_hold) {
            if ($on_hold_behavior === 'remove') {
                // Remove subscription roles
                foreach ($add_roles as $role) {
                    $this->removeUserRole($user, $role, $subscription_id);
                }
            }
            // 'keep' means we don't change roles during on-hold
        }

        // Subscription ended (cancelled, expired)
        $is_ended = $this->isEndedStatus($new_status);

        if ($is_ended && $was_active) {
            // Check if user has other active subscriptions with the same roles
            $has_other_active = $this->hasOtherActiveSubscriptionWithRoles($user_id, $add_roles);

            if (!$has_other_active) {
                // Remove subscription roles
                foreach ($add_roles as $role) {
                    $this->removeUserRole($user, $role, $subscription_id);
                }

                // Add fallback role if specified and user has no roles
                if (!empty($fallback_role) && empty($user->roles)) {
                    $this->addUserRole($user, $fallback_role, $subscription_id, true);
                }
            }
        }
    }

    /**
     * Check if user has other active subscriptions that grant the same roles
     *
     * @param int   $user_id User ID.
     * @param array $roles   Roles to check.
     * @return bool
     */
    private function hasOtherActiveSubscriptionWithRoles(int $user_id, array $roles): bool
    {
        if (empty($roles)) {
            return false;
        }

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

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

        // Get all role mapping rules
        $all_rules = arraysubs_get_setting('members_access.role_mapping_rules', []);

        foreach ($subscriptions as $subscription) {
            foreach ($all_rules as $rule) {
                if (!isset($rule['enabled']) || !$rule['enabled']) {
                    continue;
                }

                $rule_roles = $rule['add_roles'] ?? [];
                $intersection = array_intersect($roles, $rule_roles);

                if (!empty($intersection)) {
                    // Check if this rule applies to this subscription
                    $matching_rules = $this->getMatchingRules($subscription);
                    foreach ($matching_rules as $matching_rule) {
                        if ($matching_rule['id'] === $rule['id']) {
                            return true;
                        }
                    }
                }
            }
        }

        return false;
    }

    /**
     * Synchronize roles for a user based on their current subscriptions
     *
     * @param int $user_id User ID.
     * @return void
     */
    public function syncUserRoles(int $user_id): void
    {
        $user = get_userdata($user_id);
        if (!$user) {
            return;
        }

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

        $roles_to_add = [];
        $roles_to_remove = [];
        $fallback_roles = [];

        // Get all role mapping rules
        $all_rules = arraysubs_get_setting('members_access.role_mapping_rules', []);

        foreach ($all_rules as $rule) {
            if (!isset($rule['enabled']) || !$rule['enabled']) {
                continue;
            }

            $add_roles = $rule['add_roles'] ?? [];
            $remove_roles = $rule['remove_roles'] ?? [];
            $fallback_role = $rule['fallback_role'] ?? '';

            // Check if user has an active subscription matching this rule
            $has_matching_subscription = false;

            foreach ($subscriptions as $subscription) {
                $matching_rules = $this->getMatchingRules($subscription);
                foreach ($matching_rules as $matching_rule) {
                    if ($matching_rule['id'] === $rule['id']) {
                        $has_matching_subscription = true;
                        break 2;
                    }
                }
            }

            if ($has_matching_subscription) {
                $roles_to_add = array_merge($roles_to_add, $add_roles);
                $roles_to_remove = array_merge($roles_to_remove, $remove_roles);
            } else {
                // No matching subscription - roles should be removed
                $roles_to_remove = array_merge($roles_to_remove, $add_roles);

                if (!empty($fallback_role)) {
                    $fallback_roles[] = $fallback_role;
                }
            }
        }

        // Remove duplicates
        $roles_to_add = array_unique($roles_to_add);
        $roles_to_remove = array_unique($roles_to_remove);
        $fallback_roles = array_unique($fallback_roles);

        // Don't remove roles that should be added
        $roles_to_remove = array_diff($roles_to_remove, $roles_to_add);

        // Apply changes
        foreach ($roles_to_add as $role) {
            $this->addUserRole($user, $role);
        }

        foreach ($roles_to_remove as $role) {
            $this->removeUserRole($user, $role);
        }

        if (empty($user->roles)) {
            foreach ($fallback_roles as $fallback_role) {
                $this->addUserRole($user, $fallback_role, 0, true);
                break;
            }
        }
    }

    /**
     * Get enabled rules whose assigned roles are currently present on the user.
     *
     * @param int $user_id User ID.
     * @return array
     */
    private function getRulesGrantingCurrentRoles(int $user_id): array
    {
        $user = get_userdata($user_id);
        if (!$user || empty($user->roles)) {
            return [];
        }

        $all_rules = arraysubs_get_setting('members_access.role_mapping_rules', []);
        if (empty($all_rules)) {
            return [];
        }

        $rules = [];

        foreach ($all_rules as $rule) {
            if (!isset($rule['enabled']) || !$rule['enabled']) {
                continue;
            }

            $add_roles = $rule['add_roles'] ?? [];
            if (empty($add_roles) || empty(array_intersect($add_roles, $user->roles))) {
                continue;
            }

            $rules[] = $rule;
        }

        return $rules;
    }

    /**
     * Merge rule arrays without duplicates.
     *
     * @param array ...$rule_sets Rule arrays.
     * @return array
     */
    private function mergeRules(array ...$rule_sets): array
    {
        $merged = [];

        foreach ($rule_sets as $rules) {
            foreach ($rules as $rule) {
                $key = (string) ($rule['id'] ?? md5(wp_json_encode($rule)));
                $merged[$key] = $rule;
            }
        }

        return array_values($merged);
    }

    /**
     * Check whether status is access-active.
     *
     * @param string $status Subscription status.
     * @return bool
     */
    private function isActiveStatus(string $status): bool
    {
        return in_array($status, ['arraysubs-active', 'arraysubs-trial'], true);
    }

    /**
     * Check whether status is held/pending access.
     *
     * @param string $status Subscription status.
     * @return bool
     */
    private function isOnHoldStatus(string $status): bool
    {
        return in_array($status, ['arraysubs-on-hold', 'arraysubs-pending'], true);
    }

    /**
     * Check whether status has ended access.
     *
     * @param string $status Subscription status.
     * @return bool
     */
    private function isEndedStatus(string $status): bool
    {
        return in_array($status, ['arraysubs-cancelled', 'arraysubs-expired'], true);
    }

    /**
     * Add role and audit only when it changes.
     *
     * @param \WP_User $user            User object.
     * @param string   $role            Role slug.
     * @param int      $subscription_id Subscription ID.
     * @param bool     $fallback        Whether this is fallback assignment.
     * @return void
     */
    private function addUserRole(\WP_User $user, string $role, int $subscription_id = 0, bool $fallback = false): void
    {
        $role = sanitize_key($role);
        if ($role === '' || in_array($role, $user->roles, true)) {
            return;
        }

        $user->add_role($role);
        $this->logRoleChange($user, $role, 'assigned', $subscription_id, $fallback);
    }

    /**
     * Remove role and audit only when it changes.
     *
     * @param \WP_User $user            User object.
     * @param string   $role            Role slug.
     * @param int      $subscription_id Subscription ID.
     * @return void
     */
    private function removeUserRole(\WP_User $user, string $role, int $subscription_id = 0): void
    {
        $role = sanitize_key($role);
        if ($role === '' || !in_array($role, $user->roles, true)) {
            return;
        }

        $user->remove_role($role);
        $this->logRoleChange($user, $role, 'removed', $subscription_id);
    }

    /**
     * Add member audit note for role changes.
     *
     * @param \WP_User $user            User object.
     * @param string   $role            Role slug.
     * @param string   $action          assigned|removed.
     * @param int      $subscription_id Subscription ID.
     * @param bool     $fallback        Whether this is fallback assignment.
     * @return void
     */
    private function logRoleChange(\WP_User $user, string $role, string $action, int $subscription_id = 0, bool $fallback = false): void
    {
        if (!function_exists('arraysubs_add_global_note')) {
            return;
        }

        $label = $action === 'removed'
            ? __('Role removed', 'arraysubs')
            : ($fallback ? __('Role assigned (fallback)', 'arraysubs') : __('Role assigned', 'arraysubs'));

        $subscription_text = $subscription_id > 0
            ? sprintf(
                /* translators: %d: subscription ID. */
                __(' Subscription #%d.', 'arraysubs'),
                $subscription_id
            )
            : '';

        $content = sprintf(
            /* translators: 1: audit label, 2: user ID, 3: user email, 4: role slug, 5: optional subscription text. */
            __('%1$s: user #%2$d (%3$s), role %4$s.%5$s', 'arraysubs'),
            $label,
            (int) $user->ID,
            esc_html($user->user_email),
            esc_html($role),
            $subscription_text
        );

        $audit_changes = [];
        if (function_exists('arraysubs_create_audit_change_entry')) {
            $audit_changes[] = arraysubs_create_audit_change_entry(
                __('Role', 'arraysubs'),
                $action === 'removed' ? $role : '',
                $action === 'removed' ? '' : $role
            );
        }

        arraysubs_add_global_note(
            $content,
            'private',
            get_current_user_id() ?: 0,
            $audit_changes,
            'member'
        );
    }

    /**
     * Debug log for role changes
     *
     * @param int    $subscription_id Subscription ID.
     * @param int    $user_id         User ID.
     * @param string $new_status      New status.
     * @param string $old_status      Old status.
     * @return void
     */
    private function debugLog(int $subscription_id, int $user_id, string $new_status, string $old_status): void
    {
        if (!defined('WP_DEBUG') || !WP_DEBUG) {
            return;
        }
    }
}
