<?php
if ( ! defined( 'ABSPATH' ) ) { exit; }

class Vulnity_Permission_Change_Alert extends Vulnity_Alert_Base {

    private static $processed_events = array();
    private static $last_cleanup = 0;
    private static $pending_user_caps = array();

    private $roles_option_name = '';

    private $sensitive_capabilities = array(
        'manage_options',
        'edit_plugins',
        'activate_plugins',
        'install_plugins',
        'update_plugins',
        'delete_plugins',
        'edit_themes',
        'switch_themes',
        'install_themes',
        'update_themes',
        'delete_themes',
        'update_core',
        'edit_users',
        'promote_users',
        'create_users',
        'delete_users',
        'manage_network',
        'manage_network_options',
        'manage_network_plugins',
        'manage_network_themes',
        'manage_network_users',
        'manage_sites',
        'manage_privacy_options',
        'unfiltered_html',
        'unfiltered_upload'
    );

    public function __construct() {
        global $wpdb;

        $this->alert_type = 'permission_elevated';
        $this->roles_option_name = $wpdb->prefix . 'user_roles';

        parent::__construct();
    }

    protected function register_hooks() {
        add_action('updated_option', array($this, 'handle_roles_option_updated'), 10, 3);
        add_action('added_option', array($this, 'handle_roles_option_added'), 10, 2);

        add_filter('update_user_metadata', array($this, 'capture_previous_user_capabilities'), 10, 5);
        add_action('added_user_meta', array($this, 'handle_user_capabilities_added'), 10, 4);
        add_action('updated_user_meta', array($this, 'handle_user_capabilities_updated'), 10, 4);
        add_action('deleted_user_meta', array($this, 'handle_user_capabilities_deleted'), 10, 4);

        add_action('granted_super_admin', array($this, 'handle_super_admin_granted'), 10, 1);
        add_action('revoked_super_admin', array($this, 'handle_super_admin_revoked'), 10, 1);
    }

    public function handle_roles_option_added($option, $value) {
        if ($option !== $this->roles_option_name) {
            return;
        }

        $changes = $this->compare_roles(array(), $value);
        $this->maybe_create_role_permissions_alert($changes, 'roles_initialized');
    }

    public function handle_roles_option_updated($option, $old_value, $value) {
        if ($option !== $this->roles_option_name) {
            return;
        }

        $changes = $this->compare_roles($old_value, $value);
        $this->maybe_create_role_permissions_alert($changes, 'roles_updated');
    }

    public function capture_previous_user_capabilities($check, $user_id, $meta_key, $meta_value, $prev_value) {
        if (!$this->is_capabilities_meta_key($meta_key)) {
            return $check;
        }

        self::$pending_user_caps[(int) $user_id] = $this->normalize_capabilities_map(
            get_user_meta((int) $user_id, $meta_key, true)
        );

        return $check;
    }

    public function handle_user_capabilities_added($meta_id, $user_id, $meta_key, $meta_value) {
        if (!$this->is_capabilities_meta_key($meta_key)) {
            return;
        }

        $new_caps = $this->normalize_capabilities_map($meta_value);
        $changes = $this->compare_capability_maps(array(), $new_caps);
        $this->maybe_create_user_permissions_alert((int) $user_id, $changes, 'user_capabilities_added');
    }

    public function handle_user_capabilities_updated($meta_id, $user_id, $meta_key, $meta_value) {
        if (!$this->is_capabilities_meta_key($meta_key)) {
            return;
        }

        $old_caps = isset(self::$pending_user_caps[(int) $user_id])
            ? self::$pending_user_caps[(int) $user_id]
            : array();

        unset(self::$pending_user_caps[(int) $user_id]);

        $new_caps = $this->normalize_capabilities_map($meta_value);
        $changes = $this->compare_capability_maps($old_caps, $new_caps);

        $this->maybe_create_user_permissions_alert((int) $user_id, $changes, 'user_capabilities_updated');
    }

    public function handle_user_capabilities_deleted($meta_ids, $user_id, $meta_key, $meta_value) {
        if (!$this->is_capabilities_meta_key($meta_key)) {
            return;
        }

        $old_caps = $this->normalize_capabilities_map($meta_value);
        $changes = $this->compare_capability_maps($old_caps, array());

        $this->maybe_create_user_permissions_alert((int) $user_id, $changes, 'user_capabilities_deleted');
    }

    public function handle_super_admin_granted($user_id) {
        $user = get_userdata($user_id);
        if (!$user) {
            return;
        }

        $current_user_info = $this->get_current_user_info();
        $event_key = 'super_admin_granted_' . $user->ID;
        if ($this->is_duplicate($event_key)) {
            return;
        }

        $this->create_alert(array(
            'severity' => 'critical',
            'title' => 'Super Admin Privileges Granted',
            'message' => sprintf(
                'User "%s" was granted super admin privileges by "%s" from IP %s',
                $user->user_login,
                $current_user_info['user_login'],
                $current_user_info['user_ip']
            ),
            'details' => array(
                'action' => 'super_admin_granted',
                'user_id' => $user->ID,
                'user_login' => $user->user_login,
                'user_email' => $user->user_email,
                'changed_by' => $current_user_info['user_login'],
                'changed_by_id' => $current_user_info['user_id'],
                'user_ip' => $current_user_info['user_ip'],
                'user_agent' => $current_user_info['user_agent'],
                'request_uri' => $current_user_info['request_uri'],
                'timestamp' => current_time('mysql')
            )
        ));
    }

    public function handle_super_admin_revoked($user_id) {
        $user = get_userdata($user_id);
        if (!$user) {
            return;
        }

        $current_user_info = $this->get_current_user_info();
        $event_key = 'super_admin_revoked_' . $user->ID;
        if ($this->is_duplicate($event_key)) {
            return;
        }

        $this->create_alert(array(
            'severity' => 'high',
            'title' => 'Super Admin Privileges Revoked',
            'message' => sprintf(
                'User "%s" had super admin privileges revoked by "%s" from IP %s',
                $user->user_login,
                $current_user_info['user_login'],
                $current_user_info['user_ip']
            ),
            'details' => array(
                'action' => 'super_admin_revoked',
                'user_id' => $user->ID,
                'user_login' => $user->user_login,
                'user_email' => $user->user_email,
                'changed_by' => $current_user_info['user_login'],
                'changed_by_id' => $current_user_info['user_id'],
                'user_ip' => $current_user_info['user_ip'],
                'user_agent' => $current_user_info['user_agent'],
                'request_uri' => $current_user_info['request_uri'],
                'timestamp' => current_time('mysql')
            )
        ));
    }

    private function maybe_create_role_permissions_alert($changes, $action) {
        if (empty($changes)) {
            return;
        }

        $fingerprint = md5(wp_json_encode($changes));
        $event_key = 'role_permissions_' . $fingerprint;
        if ($this->is_duplicate($event_key)) {
            return;
        }

        $current_user_info = $this->get_current_user_info();
        $severity = $this->calculate_role_change_severity($changes);
        $changed_roles = array();

        foreach ($changes as $change) {
            if (!empty($change['role'])) {
                $changed_roles[] = $change['role'];
            }
        }

        $changed_roles = array_values(array_unique(array_filter($changed_roles)));

        $this->create_alert(array(
            'severity' => $severity,
            'title' => 'Role Permissions Changed',
            'message' => sprintf(
                'WordPress role permissions changed for %d role(s) by "%s" from IP %s',
                count($changed_roles),
                $current_user_info['user_login'],
                $current_user_info['user_ip']
            ),
            'details' => array(
                'action' => $action,
                'changed_roles' => $changed_roles,
                'role_changes' => $changes,
                'changed_by' => $current_user_info['user_login'],
                'changed_by_id' => $current_user_info['user_id'],
                'user_ip' => $current_user_info['user_ip'],
                'user_agent' => $current_user_info['user_agent'],
                'request_uri' => $current_user_info['request_uri'],
                'timestamp' => current_time('mysql')
            )
        ));
    }

    private function maybe_create_user_permissions_alert($user_id, $changes, $action) {
        if (empty($changes['added']) && empty($changes['removed']) && empty($changes['changed'])) {
            return;
        }

        if (!$this->has_direct_capability_changes($changes)) {
            return;
        }

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

        $fingerprint_source = array(
            'user_id' => $user_id,
            'changes' => $changes,
        );
        $event_key = 'user_permissions_' . md5(wp_json_encode($fingerprint_source));
        if ($this->is_duplicate($event_key)) {
            return;
        }

        $current_user_info = $this->get_current_user_info();
        $severity = $this->calculate_user_change_severity($changes);

        $this->create_alert(array(
            'severity' => $severity,
            'title' => 'Direct User Permissions Changed',
            'message' => sprintf(
                'Direct user capabilities for "%s" were changed by "%s" from IP %s',
                $user->user_login,
                $current_user_info['user_login'],
                $current_user_info['user_ip']
            ),
            'details' => array(
                'action' => $action,
                'user_id' => $user->ID,
                'user_login' => $user->user_login,
                'user_email' => $user->user_email,
                'direct_capability_changes' => $changes,
                'changed_by' => $current_user_info['user_login'],
                'changed_by_id' => $current_user_info['user_id'],
                'user_ip' => $current_user_info['user_ip'],
                'user_agent' => $current_user_info['user_agent'],
                'request_uri' => $current_user_info['request_uri'],
                'timestamp' => current_time('mysql')
            )
        ));
    }

    private function compare_roles($old_roles, $new_roles) {
        $old_roles = is_array($old_roles) ? $old_roles : array();
        $new_roles = is_array($new_roles) ? $new_roles : array();

        $changes = array();
        $all_roles = array_unique(array_merge(array_keys($old_roles), array_keys($new_roles)));

        foreach ($all_roles as $role) {
            $old_role = isset($old_roles[$role]) && is_array($old_roles[$role]) ? $old_roles[$role] : null;
            $new_role = isset($new_roles[$role]) && is_array($new_roles[$role]) ? $new_roles[$role] : null;

            if ($old_role === null && $new_role !== null) {
                $changes[] = array(
                    'role' => $role,
                    'change_type' => 'role_added',
                    'display_name' => isset($new_role['name']) ? $new_role['name'] : $role,
                    'added_caps' => $this->extract_enabled_caps(isset($new_role['capabilities']) ? $new_role['capabilities'] : array()),
                    'removed_caps' => array(),
                    'changed_caps' => array()
                );
                continue;
            }

            if ($old_role !== null && $new_role === null) {
                $changes[] = array(
                    'role' => $role,
                    'change_type' => 'role_removed',
                    'display_name' => isset($old_role['name']) ? $old_role['name'] : $role,
                    'added_caps' => array(),
                    'removed_caps' => $this->extract_enabled_caps(isset($old_role['capabilities']) ? $old_role['capabilities'] : array()),
                    'changed_caps' => array()
                );
                continue;
            }

            $old_caps = isset($old_role['capabilities']) ? $this->normalize_capabilities_map($old_role['capabilities']) : array();
            $new_caps = isset($new_role['capabilities']) ? $this->normalize_capabilities_map($new_role['capabilities']) : array();
            $cap_changes = $this->compare_capability_maps($old_caps, $new_caps, false);

            if (!empty($cap_changes['added']) || !empty($cap_changes['removed']) || !empty($cap_changes['changed'])) {
                $changes[] = array(
                    'role' => $role,
                    'change_type' => $this->detect_role_change_type($cap_changes),
                    'old_display_name' => isset($old_role['name']) ? $old_role['name'] : $role,
                    'new_display_name' => isset($new_role['name']) ? $new_role['name'] : $role,
                    'added_caps' => array_keys($cap_changes['added']),
                    'removed_caps' => array_keys($cap_changes['removed']),
                    'changed_caps' => $cap_changes['changed']
                );
            }
        }

        return $changes;
    }

    private function compare_capability_maps($old_caps, $new_caps, $exclude_roles = true) {
        $old_caps = $this->normalize_capabilities_map($old_caps);
        $new_caps = $this->normalize_capabilities_map($new_caps);

        if ($exclude_roles) {
            $role_names = $this->get_role_names();
            foreach ($role_names as $role_name) {
                unset($old_caps[$role_name], $new_caps[$role_name]);
            }
        }

        $changes = array(
            'added' => array(),
            'removed' => array(),
            'changed' => array()
        );

        $all_caps = array_unique(array_merge(array_keys($old_caps), array_keys($new_caps)));

        foreach ($all_caps as $cap) {
            $has_old = array_key_exists($cap, $old_caps);
            $has_new = array_key_exists($cap, $new_caps);

            if (!$has_old && $has_new) {
                $changes['added'][$cap] = $new_caps[$cap];
                continue;
            }

            if ($has_old && !$has_new) {
                $changes['removed'][$cap] = $old_caps[$cap];
                continue;
            }

            if ($has_old && $has_new && $old_caps[$cap] !== $new_caps[$cap]) {
                $changes['changed'][$cap] = array(
                    'old' => $old_caps[$cap],
                    'new' => $new_caps[$cap]
                );
            }
        }

        return $changes;
    }

    private function has_direct_capability_changes($changes) {
        return !empty($changes['added']) || !empty($changes['removed']) || !empty($changes['changed']);
    }

    private function calculate_role_change_severity($changes) {
        $highest = 'medium';

        foreach ($changes as $change) {
            $granted_caps = array_merge(
                isset($change['added_caps']) ? (array) $change['added_caps'] : array(),
                $this->extract_enabled_caps_from_changed(isset($change['changed_caps']) ? (array) $change['changed_caps'] : array())
            );

            if ($this->contains_sensitive_capability($granted_caps)) {
                return 'critical';
            }

            if ($change['change_type'] === 'role_added') {
                $highest = 'high';
            } elseif (!empty($granted_caps) && $highest !== 'high') {
                $highest = 'high';
            }
        }

        return $highest;
    }

    private function calculate_user_change_severity($changes) {
        $granted_caps = array_merge(
            array_keys((array) $changes['added']),
            $this->extract_enabled_caps_from_changed((array) $changes['changed'])
        );

        if ($this->contains_sensitive_capability($granted_caps)) {
            return 'critical';
        }

        if (!empty($granted_caps)) {
            return 'high';
        }

        return 'medium';
    }

    private function contains_sensitive_capability($capabilities) {
        foreach ((array) $capabilities as $capability) {
            if (in_array($capability, $this->sensitive_capabilities, true)) {
                return true;
            }
        }

        return false;
    }

    private function extract_enabled_caps($caps) {
        $caps = $this->normalize_capabilities_map($caps);
        $enabled = array();

        foreach ($caps as $cap => $granted) {
            if ($granted) {
                $enabled[] = $cap;
            }
        }

        return $enabled;
    }

    private function extract_enabled_caps_from_changed($changed_caps) {
        $enabled = array();

        foreach ((array) $changed_caps as $cap => $values) {
            if (isset($values['new']) && $values['new']) {
                $enabled[] = $cap;
            }
        }

        return $enabled;
    }

    private function detect_role_change_type($cap_changes) {
        $granted_caps = array_merge(
            array_keys((array) $cap_changes['added']),
            $this->extract_enabled_caps_from_changed((array) $cap_changes['changed'])
        );

        if (!empty($granted_caps)) {
            return 'role_permissions_elevated';
        }

        return 'role_permissions_reduced';
    }

    private function normalize_capabilities_map($caps) {
        if (is_string($caps)) {
            $caps = maybe_unserialize($caps);
        }

        if (!is_array($caps)) {
            return array();
        }

        $normalized = array();
        foreach ($caps as $cap => $granted) {
            if (!is_string($cap) || $cap === '') {
                continue;
            }
            $normalized[$cap] = (bool) $granted;
        }

        ksort($normalized);

        return $normalized;
    }

    private function get_role_names() {
        if (!function_exists('wp_roles')) {
            return array();
        }

        $roles = wp_roles();
        if (!$roles || !isset($roles->roles) || !is_array($roles->roles)) {
            return array();
        }

        return array_keys($roles->roles);
    }

    private function is_capabilities_meta_key($meta_key) {
        if (!is_string($meta_key) || $meta_key === '') {
            return false;
        }

        return substr($meta_key, -12) === 'capabilities';
    }

    private function is_duplicate($event_key, $window = 5) {
        $this->cleanup_old_events();

        if (isset(self::$processed_events[$event_key])) {
            $age = time() - self::$processed_events[$event_key];
            if ($age < $window) {
                return true;
            }
        }

        if (get_transient('vulnity_permission_evt_' . md5($event_key))) {
            return true;
        }

        self::$processed_events[$event_key] = time();
        set_transient('vulnity_permission_evt_' . md5($event_key), true, $window);

        return false;
    }

    private function cleanup_old_events() {
        $now = time();
        if ($now - self::$last_cleanup < 30) {
            return;
        }

        self::$last_cleanup = $now;

        foreach (self::$processed_events as $key => $timestamp) {
            if ($now - $timestamp > 10) {
                unset(self::$processed_events[$key]);
            }
        }
    }

    protected function evaluate($data) {
        return;
    }
}
