<?php
/**
 * User Events Observer
 *
 * Monitors user registration, deletion, role changes, logins, logouts,
 * profile updates, password resets, application passwords, and meta changes.
 *
 * 17 event types (including 2 multisite-only: user_marked_spam, user_unmarked_spam).
 *
 * @package VMPFence
 * @since 2.2.5
 */

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

class VMPFence_Audit_Log_Observer_User extends VMPFence_Audit_Log_Observer {

    /**
     * Register all user-related WordPress hooks.
     *
     * @return void
     */
    public function register_hooks() {
        add_action('user_register',     array($this, 'on_user_register'));
        add_action('set_user_role',     array($this, 'on_role_change'), 10, 3);
        add_action('delete_user',       array($this, 'on_user_delete'), 10, 2);
        add_action('wp_login',          array($this, 'on_login_success'), 10, 2);
        add_action('wp_login_failed',   array($this, 'on_login_failed'));
        add_action('wp_logout',         array($this, 'on_logout'));
        add_action('profile_update',    array($this, 'on_profile_update'), 10, 3);
        add_action('password_reset',    array($this, 'on_password_reset'), 10, 2);
        add_action('retrieve_password', array($this, 'on_password_reset_request'));
        add_action('set_auth_cookie',   array($this, 'on_auth_cookie_set'), 10, 6);

        // Application Passwords (WP 5.6+)
        if (version_compare(get_bloginfo('version'), '5.6', '>=')) {
            add_action('wp_create_application_password',        array($this, 'on_app_password_created'), 10, 4);
            add_action('wp_delete_application_password',        array($this, 'on_app_password_deleted'), 10, 2);
            add_action('application_password_did_authenticate', array($this, 'on_app_password_used'), 10, 2);
        }

        // User meta changes (capability/level changes)
        add_action('update_user_meta', array($this, 'on_user_meta_changed'), 10, 4);

        // Multisite-only user spam/ham
        if (is_multisite()) {
            add_action('make_spam_user', array($this, 'on_user_spam'));
            add_action('make_ham_user',  array($this, 'on_user_ham'));
        }
    }

    // =========================================================================
    // CALLBACK METHODS
    // =========================================================================

    /**
     * User registered.
     *
     * @param int $user_id
     */
    public function on_user_register($user_id) {
        $user = get_userdata($user_id);
        $this->log('user_registered', array(
            'user_id'  => $user_id,
            'username' => $user ? $user->user_login : '',
            'email'    => $user ? $user->user_email : '',
            'roles'    => $user ? $user->roles : array(),
        ), 'info');
    }

    /**
     * User role changed.
     *
     * @param int    $user_id
     * @param string $new_role
     * @param array  $old_roles
     */
    public function on_role_change($user_id, $new_role, $old_roles) {
        $user = get_userdata($user_id);
        $this->log('user_role_changed', array(
            'user_id'   => $user_id,
            'username'  => $user ? $user->user_login : '',
            'new_role'  => $new_role,
            'old_roles' => $old_roles,
        ), 'warning');
    }

    /**
     * User deleted.
     *
     * @param int      $user_id
     * @param int|null $reassign
     */
    public function on_user_delete($user_id, $reassign) {
        $user = get_userdata($user_id);
        $this->log('user_deleted', array(
            'user_id'     => $user_id,
            'username'    => $user ? $user->user_login : '',
            'reassign_to' => $reassign,
        ), 'critical');
    }

    /**
     * Successful login.
     *
     * @param string   $user_login
     * @param \WP_User $user
     */
    public function on_login_success($user_login, $user) {
        $this->log('login_success', array(
            'user_id'  => $user->ID,
            'username' => $user_login,
            'ip'       => $this->get_client_ip(),
        ), 'info');
    }

    /**
     * Failed login attempt.
     *
     * @param string $username
     */
    public function on_login_failed($username) {
        $this->log('login_failure', array(
            'username' => $username,
            'ip'       => $this->get_client_ip(),
        ), 'warning');
    }

    /**
     * User logged out.
     */
    public function on_logout() {
        $user_info = $this->get_current_user_info();
        $this->log('user_logout', $user_info, 'info');
    }

    /**
     * User profile updated.
     *
     * Filters out false positives caused by WordPress internally calling
     * wp_update_user() for non-profile actions (e.g. storing a password
     * reset activation key, or session token updates).
     *
     * wp_update_user() merges ALL existing fields into $userdata before
     * the hook fires, so we cannot rely on checking which keys are present.
     * Instead, we compare old vs new user data for meaningful field changes.
     *
     * @param int      $user_id
     * @param \WP_User $old_user_data
     * @param array    $userdata       Merged data passed to wp_insert_user() (WP 5.8+).
     */
    public function on_profile_update($user_id, $old_user_data, $userdata = array()) {
        $user = get_userdata($user_id);
        if (!$user) {
            return;
        }

        // Compare old vs new on meaningful profile fields only.
        // Excludes: user_pass, user_activation_key, user_registered,
        //           user_status — these change for internal WP operations
        //           (password reset key, password change, etc.) and are
        //           NOT real "profile edits".
        $profile_fields = array(
            'user_login', 'user_nicename', 'user_email', 'user_url',
            'display_name', 'nickname', 'first_name', 'last_name',
            'description', 'locale', 'rich_editing', 'syntax_highlighting',
            'comment_shortcuts', 'show_admin_bar_front',
        );

        $has_meaningful_change = false;
        foreach ($profile_fields as $field) {
            $old_val = isset($old_user_data->$field) ? $old_user_data->$field : '';
            $new_val = isset($user->$field) ? $user->$field : '';
            if ($old_val !== $new_val) {
                $has_meaningful_change = true;
                break;
            }
        }

        if (!$has_meaningful_change) {
            return;
        }

        $this->log('user_profile_updated', array(
            'user_id'  => $user_id,
            'username' => $user->user_login,
        ), 'info');
    }

    /**
     * Password was reset.
     *
     * @param \WP_User $user
     * @param string   $new_pass
     */
    public function on_password_reset($user, $new_pass) {
        $this->log('password_reset', array(
            'user_id'  => $user->ID,
            'username' => $user->user_login,
        ), 'warning');
    }

    /**
     * Password reset was requested.
     *
     * @param string $user_login
     */
    public function on_password_reset_request($user_login) {
        $this->log('password_reset_requested', array(
            'username' => $user_login,
            'ip'       => $this->get_client_ip(),
        ), 'info');
    }

    /**
     * Authentication cookie set.
     *
     * @param string $auth_cookie
     * @param int    $expire
     * @param int    $expiration
     * @param int    $user_id
     * @param string $scheme
     * @param string $token
     */
    public function on_auth_cookie_set($auth_cookie, $expire, $expiration, $user_id, $scheme, $token) {
        $this->log('auth_cookie_set', array(
            'user_id' => $user_id,
            'scheme'  => $scheme,
        ), 'info');
    }

    /**
     * Application password created (WP 5.6+).
     *
     * @param int    $user_id
     * @param array  $item
     * @param string $new_password
     * @param array  $args
     */
    public function on_app_password_created($user_id, $item, $new_password, $args) {
        $this->log('app_password_created', array(
            'user_id' => $user_id,
            'name'    => isset($item['name']) ? $item['name'] : '',
        ), 'warning');
    }

    /**
     * Application password deleted (WP 5.6+).
     *
     * @param int   $user_id
     * @param array $item
     */
    public function on_app_password_deleted($user_id, $item) {
        $this->log('app_password_deleted', array(
            'user_id' => $user_id,
            'name'    => isset($item['name']) ? $item['name'] : '',
        ), 'warning');
    }

    /**
     * Application password used for authentication (WP 5.6+).
     *
     * @param \WP_User $user
     * @param array    $item
     */
    public function on_app_password_used($user, $item) {
        $this->log('app_password_used', array(
            'user_id'  => $user->ID,
            'username' => $user->user_login,
        ), 'info');
    }

    /**
     * User meta changed — detects capability and level changes.
     *
     * @param int    $meta_id
     * @param int    $user_id
     * @param string $meta_key
     * @param mixed  $meta_value
     */
    public function on_user_meta_changed($meta_id, $user_id, $meta_key, $meta_value) {
        // Detect capability changes
        if ($meta_key === 'wp_capabilities' || $meta_key === $this->get_capabilities_key()) {
            $this->log('user_capabilities_changed', array(
                'user_id'  => $user_id,
                'meta_key' => $meta_key,
            ), 'warning');
        }

        // Detect user level changes
        if ($meta_key === 'wp_user_level') {
            $this->log('user_level_changed', array(
                'user_id' => $user_id,
                'level'   => $meta_value,
            ), 'info');
        }
    }

    /**
     * Multisite: user marked as spam.
     *
     * @param int $user_id
     */
    public function on_user_spam($user_id) {
        $this->log('user_marked_spam', array('user_id' => $user_id), 'warning');
    }

    /**
     * Multisite: user removed from spam.
     *
     * @param int $user_id
     */
    public function on_user_ham($user_id) {
        $this->log('user_unmarked_spam', array('user_id' => $user_id), 'info');
    }

    // =========================================================================
    // PRIVATE HELPERS
    // =========================================================================

    /**
     * Get the capabilities meta key for the current site.
     * On multisite, this includes the blog prefix (e.g. wp_2_capabilities).
     *
     * @return string
     */
    private function get_capabilities_key() {
        global $wpdb;
        return $wpdb->get_blog_prefix() . 'capabilities';
    }
}
