<?php
/**
 * Site Events Observer
 *
 * Monitors WordPress options, plugins, themes, core updates, menus,
 * file editor usage, role capabilities, data export, recovery mode,
 * and email delivery.
 *
 * 37 event types across 24 hooks (22 always + 2 "all" mode only).
 *
 * @package VMPFence
 * @since 2.2.5
 */

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

class VMPFence_Audit_Log_Observer_Site extends VMPFence_Audit_Log_Observer {

    /**
     * WordPress options to monitor with their event types and severities.
     *
     * @var array
     */
    private $monitored_options = array(
        'siteurl'                => array('event' => 'option_siteurl_changed',            'severity' => 'critical'),
        'home'                   => array('event' => 'option_home_changed',               'severity' => 'critical'),
        'admin_email'            => array('event' => 'option_admin_email_changed',        'severity' => 'critical'),
        'default_role'           => array('event' => 'option_default_role_changed',       'severity' => 'critical'),
        'users_can_register'     => array('event' => 'option_registration_changed',       'severity' => 'critical'),
        // Note: active_plugins, template, stylesheet are NOT monitored here
        // because plugin_activated/plugin_deactivated and theme_changed already cover them.
        // 'active_plugins'         => array('event' => 'option_active_plugins_changed',     'severity' => 'critical'),
        // 'template'               => array('event' => 'option_template_changed',           'severity' => 'critical'),
        // 'stylesheet'             => array('event' => 'option_stylesheet_changed',         'severity' => 'critical'),
        'blogname'               => array('event' => 'option_blogname_changed',           'severity' => 'info'),
        'blogdescription'        => array('event' => 'option_description_changed',        'severity' => 'info'),
        'permalink_structure'    => array('event' => 'option_permalink_changed',          'severity' => 'warning'),
        'blog_public'            => array('event' => 'option_search_visibility_changed',  'severity' => 'warning'),
        'comment_registration'   => array('event' => 'option_comment_reg_changed',        'severity' => 'info'),
        'default_comment_status' => array('event' => 'option_default_comments_changed',   'severity' => 'info'),
        'comment_moderation'     => array('event' => 'option_comment_moderation_changed', 'severity' => 'info'),
    );

    /**
     * Register all site-related WordPress hooks.
     *
     * @return void
     */
    public function register_hooks() {
        // Per-option monitoring (15 hooks)
        foreach ($this->monitored_options as $option => $config) {
            add_action("update_option_{$option}", array($this, 'on_option_updated'), 10, 3);
        }

        // Plugins
        add_action('activated_plugin',          array($this, 'on_plugin_activated'));
        add_action('deactivated_plugin',        array($this, 'on_plugin_deactivated'));
        add_action('upgrader_process_complete',  array($this, 'on_upgrader_complete'), 10, 2);
        add_action('deleted_plugin',            array($this, 'on_plugin_deleted'), 10, 2);

        // Themes
        add_action('switch_theme', array($this, 'on_theme_switched'), 10, 3);
        if (version_compare(get_bloginfo('version'), '5.8', '>=')) {
            add_action('deleted_theme', array($this, 'on_theme_deleted'));
        }
        add_action('customize_save_after', array($this, 'on_customizer_saved'));

        // Core
        add_action('_core_updated_successfully', array($this, 'on_core_updated'));
        add_action('automatic_updates_complete', array($this, 'on_auto_updates'));

        // Menus
        add_action('wp_update_nav_menu', array($this, 'on_menu_updated'));
        add_action('wp_create_nav_menu', array($this, 'on_menu_created'));
        add_action('wp_delete_nav_menu', array($this, 'on_menu_deleted'));

        // File editor
        add_action('wp_edit_theme_plugin_file', array($this, 'on_file_edited'), 10, 2);

        // Permissions
        add_action('update_option_wp_user_roles', array($this, 'on_role_capabilities_changed'), 10, 3);
        add_action('user_has_cap',                array($this, 'on_admin_page_denied'), 10, 4);

        // Data export & recovery
        add_action('export_wp',                  array($this, 'on_data_exported'));
        add_action('generate_recovery_mode_key', array($this, 'on_recovery_key'));

        // Mail (only in "all" mode — too noisy otherwise)
        if ($this->logging_mode === 'all') {
            add_action('wp_mail_succeeded', array($this, 'on_mail_sent'));
            add_action('wp_mail_failed',    array($this, 'on_mail_failed'));
        }
    }

    // =========================================================================
    // OPTIONS CALLBACK
    // =========================================================================

    /**
     * A monitored option was updated.
     *
     * @param mixed  $old_value
     * @param mixed  $new_value
     * @param string $option
     */
    public function on_option_updated($old_value, $new_value, $option) {
        if (!isset($this->monitored_options[$option])) {
            return;
        }

        $config = $this->monitored_options[$option];

        // Don't log if value didn't actually change
        if ($old_value === $new_value) {
            return;
        }

        $this->log($config['event'], array(
            'option'    => $option,
            'old_value' => is_array($old_value) ? wp_json_encode($old_value) : $old_value,
            'new_value' => is_array($new_value) ? wp_json_encode($new_value) : $new_value,
        ), $config['severity']);
    }

    // =========================================================================
    // PLUGIN CALLBACKS
    // =========================================================================

    /**
     * Plugin activated.
     *
     * @param string $plugin Plugin basename (e.g. "plugin-dir/plugin-file.php").
     */
    public function on_plugin_activated($plugin) {
        $plugin_data = get_plugin_data(WP_PLUGIN_DIR . '/' . $plugin, false, false);
        $this->log('plugin_activated', array(
            'plugin'  => $plugin,
            'name'    => isset($plugin_data['Name']) ? $plugin_data['Name'] : $plugin,
            'version' => isset($plugin_data['Version']) ? $plugin_data['Version'] : '',
        ), 'warning');
    }

    /**
     * Plugin deactivated.
     *
     * @param string $plugin
     */
    public function on_plugin_deactivated($plugin) {
        $plugin_data = get_plugin_data(WP_PLUGIN_DIR . '/' . $plugin, false, false);
        $this->log('plugin_deactivated', array(
            'plugin'  => $plugin,
            'name'    => isset($plugin_data['Name']) ? $plugin_data['Name'] : $plugin,
        ), 'warning');
    }

    /**
     * Plugin or theme installed/updated via the upgrader.
     *
     * @param \WP_Upgrader $upgrader
     * @param array        $options
     */
    public function on_upgrader_complete($upgrader, $options) {
        $action = isset($options['action']) ? $options['action'] : '';
        $type   = isset($options['type'])   ? $options['type']   : '';

        if ($action === 'install') {
            // Installation
            if ($type === 'plugin') {
                $this->log('plugin_installed', array(
                    'type' => 'plugin',
                ), 'info');
            } elseif ($type === 'theme') {
                $this->log('theme_installed', array(
                    'type' => 'theme',
                ), 'info');
            }
            return;
        }

        if ($action === 'update') {
            // Update
            if ($type === 'plugin' && !empty($options['plugins'])) {
                foreach ($options['plugins'] as $plugin) {
                    $this->log('plugin_updated', array(
                        'plugin' => $plugin,
                    ), 'info');
                }
            } elseif ($type === 'theme' && !empty($options['themes'])) {
                foreach ($options['themes'] as $theme) {
                    $this->log('theme_updated', array(
                        'theme' => $theme,
                    ), 'info');
                }
            }
        }
    }

    /**
     * Plugin deleted.
     *
     * @param string $plugin
     * @param bool   $deleted
     */
    public function on_plugin_deleted($plugin, $deleted) {
        if ($deleted) {
            $this->log('plugin_deleted', array(
                'plugin' => $plugin,
            ), 'warning');
        }
    }

    // =========================================================================
    // THEME CALLBACKS
    // =========================================================================

    /**
     * Theme switched.
     *
     * @param string    $new_name
     * @param \WP_Theme $new_theme
     * @param \WP_Theme $old_theme
     */
    public function on_theme_switched($new_name, $new_theme, $old_theme) {
        $this->log('theme_changed', array(
            'new_theme' => $new_name,
            'new_slug'  => $new_theme->get_stylesheet(),
            'old_theme' => $old_theme->get('Name'),
            'old_slug'  => $old_theme->get_stylesheet(),
        ), 'warning');
    }

    /**
     * Theme deleted (WP 5.8+).
     *
     * @param string $stylesheet
     */
    public function on_theme_deleted($stylesheet) {
        $this->log('theme_deleted', array(
            'stylesheet' => $stylesheet,
        ), 'warning');
    }

    /**
     * Customizer saved.
     *
     * @param \WP_Customize_Manager $wp_customize
     */
    public function on_customizer_saved($wp_customize) {
        $this->log('customizer_saved', array(
            'theme' => $wp_customize->get_stylesheet(),
        ), 'info');
    }

    // =========================================================================
    // CORE CALLBACKS
    // =========================================================================

    /**
     * WordPress core updated successfully.
     *
     * @param string $wp_version
     */
    public function on_core_updated($wp_version) {
        $this->log('core_updated', array(
            'new_version' => $wp_version,
        ), 'warning');
    }

    /**
     * Automatic updates completed.
     *
     * @param array $results
     */
    public function on_auto_updates($results) {
        $summary = array();
        foreach (array('core', 'plugin', 'theme', 'translation') as $type) {
            if (!empty($results[$type])) {
                $summary[$type] = count($results[$type]);
            }
        }
        $this->log('auto_updates_completed', array(
            'summary' => $summary,
        ), 'info');
    }

    // =========================================================================
    // MENU CALLBACKS
    // =========================================================================

    /**
     * Navigation menu updated.
     *
     * @param int $menu_id
     */
    public function on_menu_updated($menu_id) {
        $menu = wp_get_nav_menu_object($menu_id);
        $this->log('menu_updated', array(
            'menu_id' => $menu_id,
            'name'    => $menu ? $menu->name : '',
        ), 'info');
    }

    /**
     * Navigation menu created.
     *
     * @param int $menu_id
     */
    public function on_menu_created($menu_id) {
        $menu = wp_get_nav_menu_object($menu_id);
        $this->log('menu_created', array(
            'menu_id' => $menu_id,
            'name'    => $menu ? $menu->name : '',
        ), 'info');
    }

    /**
     * Navigation menu deleted.
     *
     * @param int $menu_id
     */
    public function on_menu_deleted($menu_id) {
        $this->log('menu_deleted', array(
            'menu_id' => $menu_id,
        ), 'info');
    }

    // =========================================================================
    // FILE EDITOR CALLBACK
    // =========================================================================

    /**
     * Plugin/theme file edited via the built-in editor.
     *
     * @param string $file_path
     * @param array  $args
     */
    public function on_file_edited($file_path, $args) {
        $this->log('file_editor_used', array(
            'file'   => $file_path,
            'plugin' => isset($args['plugin']) ? $args['plugin'] : '',
            'theme'  => isset($args['theme'])  ? $args['theme']  : '',
        ), 'critical');
    }

    // =========================================================================
    // PERMISSION CALLBACKS
    // =========================================================================

    /**
     * Role capabilities changed (wp_user_roles option updated).
     *
     * @param mixed  $old_value
     * @param mixed  $new_value
     * @param string $option
     */
    public function on_role_capabilities_changed($old_value, $new_value, $option) {
        if ($old_value === $new_value) {
            return;
        }
        $this->log('role_capabilities_changed', array(
            'option' => $option,
        ), 'critical');
    }

    /**
     * Admin page access denied.
     * Fires on every user_has_cap check — we only log actual denials on admin pages.
     *
     * @param array  $allcaps
     * @param array  $caps
     * @param array  $args
     * @param object $user
     * @return array Always returns $allcaps unmodified.
     */
    public function on_admin_page_denied($allcaps, $caps, $args, $user) {
        static $already_logged = false;

        // Only check in admin context — skip AJAX, cron, and REST
        if (!is_admin() || wp_doing_ajax() || wp_doing_cron() || (defined('REST_REQUEST') && REST_REQUEST)) {
            return $allcaps;
        }

        // Only fire once per request
        if ($already_logged) {
            return $allcaps;
        }

        // Only fire for manage_options capability checks (admin pages)
        if (empty($caps) || !in_array('manage_options', $caps, true)) {
            return $allcaps;
        }

        // Skip if no real user is logged in (avoids System/anonymous noise)
        if (!isset($user->ID) || $user->ID < 1) {
            return $allcaps;
        }

        // Check if the user actually lacks the capability
        $has_cap = true;
        foreach ($caps as $cap) {
            if (empty($allcaps[$cap])) {
                $has_cap = false;
                break;
            }
        }

        if (!$has_cap) {
            $already_logged = true;
            $this->log('admin_page_denied', array(
                'user_id'  => $user->ID,
                'caps'     => $caps,
                'page'     => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '',
            ), 'warning');
        }

        return $allcaps;
    }

    // =========================================================================
    // DATA EXPORT & RECOVERY
    // =========================================================================

    /**
     * WordPress data exported.
     *
     * @param array $args
     */
    public function on_data_exported($args = array()) {
        $user_info = $this->get_current_user_info();
        $this->log('site_data_exported', array_merge($user_info, array(
            'content' => isset($args['content']) ? $args['content'] : 'all',
        )), 'warning');
    }

    /**
     * Recovery mode key generated.
     */
    public function on_recovery_key() {
        $this->log('recovery_key_generated', array(
            'ip' => $this->get_client_ip(),
        ), 'critical');
    }

    // =========================================================================
    // MAIL CALLBACKS (only in "all" mode)
    // =========================================================================

    /**
     * Email sent successfully.
     *
     * @param array $mail_data
     */
    public function on_mail_sent($mail_data) {
        $this->log('mail_sent', array(
            'to'      => isset($mail_data['to'])      ? $mail_data['to']      : '',
            'subject' => isset($mail_data['subject'])  ? $mail_data['subject'] : '',
        ), 'info');
    }

    /**
     * Email failed to send.
     *
     * @param \WP_Error $error
     */
    public function on_mail_failed($error) {
        $this->log('mail_failed', array(
            'error' => is_wp_error($error) ? $error->get_error_message() : 'Unknown error',
        ), 'warning');
    }
}
