<?php
/**
 * Logger functionality for Iron Security
 *
 * @link       https://wpiron.com
 * @since      1.0.0
 *
 * @package    iron-security
 * @subpackage iron-security/includes
 */

class Iron_Security_Logger {

    /**
     * Log types constants
     */
    const LOG_TYPE_LOGIN = 'login';
    const LOG_TYPE_FAILED_LOGIN = 'failed_login';
    const LOG_TYPE_LOCKOUT = 'lockout';
    const LOG_TYPE_2FA = 'two_factor_auth';
    const LOG_TYPE_SETTINGS = 'settings_change';
    const LOG_TYPE_FILE_CHANGE = 'file_change';
    const LOG_TYPE_USER_ENUM = 'user_enumeration';
    const LOG_TYPE_REST_API = 'rest_api';
    const LOG_TYPE_XML_RPC = 'xml_rpc';
    const LOG_TYPE_SECURITY = 'security';
    const LOG_TYPE_ADMIN_ACTION = 'admin_action';
    const LOG_TYPE_USER_CREATED = 'user_created';
    const LOG_TYPE_USER_MODIFIED = 'user_modified';
    const LOG_TYPE_PLUGIN_ACTION = 'plugin_action';
    const LOG_TYPE_MALICIOUS_REQUEST = 'malicious_request';
    const LOG_TYPE_ROLE_CHANGE = 'role_change';
    const LOG_TYPE_CLICKJACK_ATTEMPT = 'clickjack_attempt';
    const LOG_TYPE_API_REQUEST = 'api_request';
    const LOG_TYPE_SUSPICIOUS_BEHAVIOR = 'suspicious_behavior';
    const LOG_TYPE_HTTP_ERROR = 'http_error';
    const LOG_TYPE_CORE_UPDATE = 'core_update';
    const LOG_TYPE_THEME_ACTION = 'theme_action';

    /**
     * Status constants
     */
    const STATUS_SUCCESS = 'success';
    const STATUS_FAILURE = 'failure';
    const STATUS_WARNING = 'warning';
    const STATUS_INFO = 'info';

    /**
     * Add a log entry
     *
     * @param string $log_type Type of log
     * @param string $message The log message
     * @param string $status Status of the action (success, failure, etc)
     * @param int|null $user_id User ID or null if not available
     * @param string|null $username Username or null if not available
     * @param array $extra_data Additional data to store with the log
     * 
     * @return int|false The ID of the inserted row, or false on failure
     */
    public static function add_log($log_type, $message, $status = 'info', $user_id = null, $username = null, $extra_data = array()) {
        global $wpdb;
        
        // Get current user if not provided
        if ($user_id === null && is_user_logged_in()) {
            $user_id = get_current_user_id();
        }
        
        // Get username if not provided but user ID is
        if ($username === null && $user_id) {
            $user = get_user_by('id', $user_id);
            if ($user) {
                $username = $user->user_login;
            }
        }

        // Get client IP address
        $ip_address = self::get_client_ip();
        
        // Serialize extra data if provided
        $serialized_data = !empty($extra_data) ? maybe_serialize($extra_data) : null;
        
        // Insert log entry
        $result = $wpdb->insert(
            $wpdb->prefix . 'iron_security_logs',
            array(
                'log_type' => $log_type,
                'user_id' => $user_id,
                'username' => $username,
                'ip_address' => $ip_address,
                'message' => $message,
                'status' => $status,
                'date_created' => current_time('mysql'),
                'extra_data' => $serialized_data
            ),
            array('%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s')
        );
        
        return $result ? $wpdb->insert_id : false;
    }
    
    /**
     * Log a security event
     *
     * @param string $event_type Type of security event
     * @param string $message The log message
     * @param string $status Status of the event (default: 'info')
     * @return int|false The ID of the inserted log entry, or false on failure
     */
    public static function log_security_event($event_type, $message, $status = 'info') {
        return self::add_log(
            self::LOG_TYPE_SECURITY,
            $message,
            $status,
            null,
            null,
            array('event_type' => $event_type)
        );
    }
    
    /**
     * Log successful login
     *
     * @param string $username The username that successfully logged in
     * @param int $user_id User ID
     * @return int|false The ID of the inserted log entry, or false on failure
     */
    public static function log_successful_login($username, $user_id) {
        return self::add_log(
            self::LOG_TYPE_LOGIN,
            sprintf('User "%s" logged in successfully', $username),
            self::STATUS_SUCCESS,
            $user_id,
            $username
        );
    }
    
    /**
     * Log failed login attempt
     *
     * @param string $username The username that failed to log in
     * @return int|false The ID of the inserted log entry, or false on failure
     */
    public static function log_failed_login($username) {
        return self::add_log(
            self::LOG_TYPE_FAILED_LOGIN,
            sprintf('Failed login attempt for user "%s"', $username),
            self::STATUS_FAILURE,
            null,
            $username
        );
    }
    
    /**
     * Log login lockout
     *
     * @param string $username The username that was locked out
     * @param int $attempts The number of failed attempts
     * @return int|false The ID of the inserted log entry, or false on failure
     */
    public static function log_lockout($username, $attempts) {
        return self::add_log(
            self::LOG_TYPE_LOCKOUT,
            sprintf('User "%s" locked out after %d failed login attempts', $username, $attempts),
            self::STATUS_WARNING,
            null,
            $username,
            array('attempts' => $attempts)
        );
    }
    
    /**
     * Log 2FA authentication
     *
     * @param string $username The username
     * @param int $user_id User ID
     * @param bool $success Whether authentication was successful
     * @return int|false The ID of the inserted log entry, or false on failure
     */
    public static function log_2fa($username, $user_id, $success = true) {
        return self::add_log(
            self::LOG_TYPE_2FA,
            sprintf('2FA authentication %s for user "%s"', 
                $success ? 'successful' : 'failed', 
                $username
            ),
            $success ? self::STATUS_SUCCESS : self::STATUS_FAILURE,
            $user_id,
            $username
        );
    }
    
    /**
     * Log settings change
     *
     * @param string $setting The setting that was changed
     * @param mixed $old_value The old value
     * @param mixed $new_value The new value
     * @return int|false The ID of the inserted log entry, or false on failure
     */
    public static function log_settings_change($setting, $old_value, $new_value) {
        return self::add_log(
            self::LOG_TYPE_SETTINGS,
            sprintf('Setting "%s" changed', $setting),
            self::STATUS_INFO,
            null,
            null,
            array(
                'setting' => $setting,
                'old_value' => $old_value,
                'new_value' => $new_value
            )
        );
    }
    
    /**
     * Get logs from the database
     *
     * @param array $args {
     *     Optional. Arguments to retrieve logs.
     *
     *     @type int    $per_page        Number of logs per page. Default 20.
     *     @type int    $page            Current page number. Default 1.
     *     @type string $log_type        Filter by log type. Default empty (all).
     *     @type string $status          Filter by status. Default empty (all).
     *     @type string $search          Search by username or message. Default empty.
     *     @type string $ip_address      Filter by IP address. Default empty.
     *     @type string $user_id         Filter by user ID. Default empty.
     *     @type string $date_from       Filter by date from. Default empty.
     *     @type string $date_to         Filter by date to. Default empty.
     * }
     * @return array Logs and log count
     */
    public static function get_logs($args = array()) {
        global $wpdb;

        $defaults = array(
            'per_page'   => 20,
            'page'       => 1,
            'log_type'   => '',
            'status'     => '',
            'search'     => '',
            'ip_address' => '',
            'user_id'    => '',
            'date_from'  => '',
            'date_to'    => '',
        );

        $args = wp_parse_args($args, $defaults);

        // Sanitize inputs
        $per_page   = absint($args['per_page']);
        $page       = absint($args['page']);
        $log_type   = sanitize_text_field($args['log_type']);
        $status     = sanitize_text_field($args['status']);
        $search     = sanitize_text_field($args['search']);
        $ip_address = sanitize_text_field($args['ip_address']);
        $user_id    = absint($args['user_id']);
        $date_from  = sanitize_text_field($args['date_from']);
        $date_to    = sanitize_text_field($args['date_to']);

        // Calculate offset for pagination
        $offset = ($page - 1) * $per_page;

        // Build query conditions
        $where = [];
        $where_args = [];

        if (!empty($log_type)) {
            $where[] = 'log_type = %s';
            $where_args[] = $log_type;
        }

        if (!empty($status)) {
            $where[] = 'status = %s';
            $where_args[] = $status;
        }

        if (!empty($search)) {
            $where[] = '(message LIKE %s OR username LIKE %s)';
            $search_term = '%' . $wpdb->esc_like($search) . '%';
            $where_args[] = $search_term;
            $where_args[] = $search_term;
        }

        if (!empty($ip_address)) {
            $where[] = 'ip_address = %s';
            $where_args[] = $ip_address;
        }

        if (!empty($user_id)) {
            $where[] = 'user_id = %d';
            $where_args[] = $user_id;
        }

        if (!empty($date_from)) {
            $where[] = 'date_created >= %s';
            $where_args[] = $date_from . ' 00:00:00';
        }

        if (!empty($date_to)) {
            $where[] = 'date_created <= %s';
            $where_args[] = $date_to . ' 23:59:59';
        }

        // Build the where clause
        $where_clause = '';
        if (!empty($where)) {
            $where_clause = 'WHERE ' . implode(' AND ', $where);
        }

        // Get total count
        $count_query = "SELECT COUNT(*) FROM {$wpdb->prefix}iron_security_logs $where_clause";
        if (!empty($where_args)) {
            $count_query = $wpdb->prepare($count_query, $where_args);
        }
        $total = $wpdb->get_var($count_query);

        // Get logs
        $query = "SELECT * FROM {$wpdb->prefix}iron_security_logs $where_clause ORDER BY date_created DESC LIMIT %d OFFSET %d";
        $prepared_args = array_merge($where_args, [$per_page, $offset]);
        $query = $wpdb->prepare($query, $prepared_args);
        $logs = $wpdb->get_results($query);

        // Format and prepare logs for display
        $formatted_logs = [];
        foreach ($logs as $log) {
            // Handle extra_data field properly
            $extra_data = maybe_unserialize($log->extra_data);
            
            // If it's an array, ensure it's properly formatted for JSON
            if (is_array($extra_data)) {
                // Clean up the array - make sure all values are properly encoded for JSON
                foreach ($extra_data as $key => $value) {
                    // Convert any objects to array
                    if (is_object($value)) {
                        $extra_data[$key] = (array)$value;
                    }
                    
                    // Handle any special types that JSON might not support
                    if ($value instanceof \__PHP_Incomplete_Class) {
                        $extra_data[$key] = 'Incomplete class data';
                    }
                }
                
                // Make sure we can json_encode this data later
                $json_test = json_encode($extra_data);
                if ($json_test === false) {
                    // If encoding failed, provide a simplified version
                    $extra_data = [
                        'data_error' => 'The extra data contains elements that cannot be properly displayed.',
                        'data_keys' => array_keys($extra_data)
                    ];
                }
            } elseif (is_object($extra_data)) {
                // Convert objects to arrays
                $extra_data = (array)$extra_data;
            } elseif (!empty($extra_data) && is_string($extra_data) && !is_serialized($extra_data)) {
                // If it's a string that is not serialized, try to parse as JSON
                $json_data = json_decode($extra_data, true);
                if ($json_data !== null) {
                    $extra_data = $json_data;
                }
            }
            
            $formatted_log = [
                'id'           => $log->id,
                'log_type'     => $log->log_type,
                'message'      => $log->message,
                'status'       => $log->status,
                'user_id'      => $log->user_id,
                'username'     => $log->username,
                'ip_address'   => $log->ip_address,
                'date_created' => $log->date_created,
                'extra_data'   => $extra_data
            ];
            
            $formatted_logs[] = $formatted_log;
        }

        return [
            'logs'      => $formatted_logs,
            'total'     => absint($total),
            'page'      => $page,
            'per_page'  => $per_page,
            'pages'     => ceil($total / $per_page)
        ];
    }
    
    /**
     * Get all available log types
     *
     * @return array Log types
     */
    public static function get_log_types() {
        return array(
            self::LOG_TYPE_LOGIN => 'Successful Logins',
            self::LOG_TYPE_FAILED_LOGIN => 'Failed Logins',
            self::LOG_TYPE_LOCKOUT => 'Lockouts',
            self::LOG_TYPE_2FA => 'Two-Factor Authentication',
            self::LOG_TYPE_SETTINGS => 'Settings Changes',
            self::LOG_TYPE_FILE_CHANGE => 'File Changes',
            self::LOG_TYPE_USER_ENUM => 'User Enumeration Attempts',
            self::LOG_TYPE_REST_API => 'REST API Access',
            self::LOG_TYPE_XML_RPC => 'XML-RPC Access',
            self::LOG_TYPE_SECURITY => 'Security Events',
            self::LOG_TYPE_ADMIN_ACTION => 'Admin Actions',
            self::LOG_TYPE_USER_CREATED => 'User Creation',
            self::LOG_TYPE_USER_MODIFIED => 'User Modification',
            self::LOG_TYPE_PLUGIN_ACTION => 'Plugin Actions',
            self::LOG_TYPE_MALICIOUS_REQUEST => 'Malicious Requests',
            self::LOG_TYPE_ROLE_CHANGE => 'Role Changes',
            self::LOG_TYPE_CLICKJACK_ATTEMPT => 'Clickjack Attempts',
            self::LOG_TYPE_API_REQUEST => 'API Requests',
            self::LOG_TYPE_SUSPICIOUS_BEHAVIOR => 'Suspicious Behavior',
            self::LOG_TYPE_HTTP_ERROR => 'HTTP Errors',
            self::LOG_TYPE_CORE_UPDATE => 'Core Updates',
            self::LOG_TYPE_THEME_ACTION => 'Theme Actions'
        );
    }
    
    /**
     * Get available status types
     *
     * @return array Array of status types and their labels
     */
    public static function get_status_types() {
        return array(
            self::STATUS_SUCCESS => 'Success',
            self::STATUS_FAILURE => 'Failure',
            self::STATUS_WARNING => 'Warning',
            self::STATUS_INFO => 'Information'
        );
    }
    
    /**
     * Clear logs
     *
     * @param string $log_type Optional log type to clear, or empty to clear all
     * @return bool True if logs were cleared, false otherwise
     */
    public static function clear_logs($log_type = '') {
        global $wpdb;
        $table = $wpdb->prefix . 'iron_security_logs';
        
        if (empty($log_type)) {
            // Clear all logs
            $result = $wpdb->query("TRUNCATE TABLE $table");
        } else {
            // Clear specific log type
            $result = $wpdb->delete($table, array('log_type' => $log_type), array('%s'));
        }
        
        return $result !== false;
    }
    
    /**
     * Get client IP address
     *
     * @return string Client IP address
     */
	private static function get_client_ip() {
		$trusted_proxies = [];

		$remote_addr = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';

		if (in_array($remote_addr, $trusted_proxies)) {
			if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
				$forwarded_ips = array_map('trim', explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']));
				foreach ($forwarded_ips as $ip) {
					if (self::validate_ip($ip)) {
						return $ip;
					}
				}
			}
		}

		if (self::validate_ip($remote_addr)) {
			return $remote_addr;
		}

		return '0.0.0.0';
	}
    

	private static function validate_ip($ip) {
		return filter_var($ip, FILTER_VALIDATE_IP) !== false;
	}

    /**
     * Log user creation
     *
     * @param int $user_id The new user ID
     * @param array $userdata The user data
     * @return int|false The ID of the inserted log entry, or false on failure
     */
    public static function log_user_created($user_id, $userdata = array()) {
        $user = get_userdata($user_id);
        $username = $user ? $user->user_login : 'Unknown';
        $creator_id = get_current_user_id();
        $creator = get_userdata($creator_id);
        $creator_name = $creator ? $creator->user_login : 'System';

        return self::add_log(
            self::LOG_TYPE_USER_CREATED,
            sprintf('New user "%s" created by %s', $username, $creator_name),
            self::STATUS_INFO,
            $creator_id,
            $creator_name,
            array(
                'new_user_id' => $user_id,
                'new_username' => $username,
                'user_role' => isset($userdata['role']) ? $userdata['role'] : 'Unknown',
                'user_email' => isset($userdata['user_email']) ? $userdata['user_email'] : 'Unknown'
            )
        );
    }

    /**
     * Log user role change
     *
     * @param int $user_id The user ID
     * @param string $new_role The new role
     * @param array $old_roles The old roles
     * @return int|false The ID of the inserted log entry, or false on failure
     */
    public static function log_role_change($user_id, $new_role, $old_roles = array()) {
        $user = get_userdata($user_id);
        $username = $user ? $user->user_login : 'Unknown';
        $changer_id = get_current_user_id();
        $changer = get_userdata($changer_id);
        $changer_name = $changer ? $changer->user_login : 'System';

        return self::add_log(
            self::LOG_TYPE_ROLE_CHANGE,
            sprintf('User "%s" role changed from "%s" to "%s" by %s', 
                $username, 
                implode(', ', $old_roles), 
                $new_role,
                $changer_name
            ),
            self::STATUS_INFO,
            $changer_id,
            $changer_name,
            array(
                'target_user_id' => $user_id,
                'target_username' => $username,
                'old_roles' => $old_roles,
                'new_role' => $new_role
            )
        );
    }

    /**
     * Log plugin activation/deactivation/update
     *
     * @param string $action The action (activate, deactivate, update)
     * @param string $plugin The plugin path
     * @return int|false The ID of the inserted log entry, or false on failure
     */
    public static function log_plugin_action($action, $plugin) {
        $plugin_data = get_plugin_data(WP_PLUGIN_DIR . '/' . $plugin);
        $plugin_name = isset($plugin_data['Name']) ? $plugin_data['Name'] : $plugin;

        return self::add_log(
            self::LOG_TYPE_PLUGIN_ACTION,
            sprintf('Plugin "%s" %s', $plugin_name, $action),
            self::STATUS_INFO,
            null,
            null,
            array(
                'plugin' => $plugin,
                'plugin_name' => $plugin_name,
                'action' => $action,
                'version' => isset($plugin_data['Version']) ? $plugin_data['Version'] : 'Unknown'
            )
        );
    }

    /**
     * Log malicious request attempts
     *
     * @param string $request_type Type of malicious request
     * @param string $details Details about the request
     * @return int|false The ID of the inserted log entry, or false on failure
     */
    public static function log_malicious_request($request_type, $details = '') {
        return self::add_log(
            self::LOG_TYPE_MALICIOUS_REQUEST,
            sprintf('Malicious request detected: %s', $request_type),
            self::STATUS_WARNING,
            null,
            null,
            array(
                'request_type' => $request_type,
                'request_details' => $details,
                'http_referer' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '',
                'request_uri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '',
                'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''
            )
        );
    }

    /**
     * Log suspicious behavior
     *
     * @param string $behavior_type Type of suspicious behavior
     * @param string $details Details about the behavior
     * @return int|false The ID of the inserted log entry, or false on failure
     */
    public static function log_suspicious_behavior($behavior_type, $details = '') {
        return self::add_log(
            self::LOG_TYPE_SUSPICIOUS_BEHAVIOR,
            sprintf('Suspicious behavior detected: %s', $behavior_type),
            self::STATUS_WARNING,
            null,
            null,
            array(
                'behavior_type' => $behavior_type,
                'behavior_details' => $details,
                'http_referer' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '',
                'request_uri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '',
                'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''
            )
        );
    }

    /**
     * Log clickjack attempt
     *
     * @param string $details Details about the attempt
     * @return int|false The ID of the inserted log entry, or false on failure
     */
    public static function log_clickjack_attempt($details = '') {
        return self::add_log(
            self::LOG_TYPE_CLICKJACK_ATTEMPT,
            'Potential clickjacking attempt detected',
            self::STATUS_WARNING,
            null,
            null,
            array(
                'details' => $details,
                'http_referer' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '',
                'request_uri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '',
                'request_method' => isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : '',
                'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''
            )
        );
    }

    /**
     * Log admin action
     *
     * @param string $action The admin action
     * @param array $details Additional details
     * @return int|false The ID of the inserted log entry, or false on failure
     */
    public static function log_admin_action($action, $details = array()) {
        return self::add_log(
            self::LOG_TYPE_ADMIN_ACTION,
            sprintf('Admin action: %s', $action),
            self::STATUS_INFO,
            null,
            null,
            array_merge(array(
                'action' => $action,
                'http_referer' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '',
                'request_uri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''
            ), $details)
        );
    }

    /**
     * Log theme action (activation, update, etc)
     *
     * @param string $action The action performed
     * @param string $theme The theme slug or name
     * @param array $details Additional details
     * @return int|false The ID of the inserted log entry, or false on failure
     */
    public static function log_theme_action($action, $theme, $details = array()) {
        return self::add_log(
            self::LOG_TYPE_THEME_ACTION,
            sprintf('Theme "%s" %s', $theme, $action),
            self::STATUS_INFO,
            null,
            null,
            array_merge(array(
                'action' => $action,
                'theme' => $theme
            ), $details)
        );
    }

    /**
     * Log API request
     *
     * @param string $api The API being accessed
     * @param string $endpoint The specific endpoint
     * @param string $method The HTTP method
     * @return int|false The ID of the inserted log entry, or false on failure
     */
    public static function log_api_request($api, $endpoint, $method = 'GET') {
        return self::add_log(
            self::LOG_TYPE_API_REQUEST,
            sprintf('%s request to %s API endpoint: %s', $method, $api, $endpoint),
            self::STATUS_INFO,
            null,
            null,
            array(
                'api' => $api,
                'endpoint' => $endpoint,
                'method' => $method,
                'request_uri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '',
                'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''
            )
        );
    }

    /**
     * Log HTTP error
     * 
     * @param int $status_code HTTP status code
     * @param string $message Error message
     * @return int|false The ID of the inserted log entry, or false on failure
     */
    public static function log_http_error($status_code, $message = '') {
        return self::add_log(
            self::LOG_TYPE_HTTP_ERROR,
            sprintf('HTTP Error %d: %s', $status_code, $message),
            self::STATUS_FAILURE,
            null,
            null,
            array(
                'status_code' => $status_code,
                'message' => $message,
                'request_uri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '',
                'http_referer' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '',
                'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''
            )
        );
    }

    /**
     * Log core WordPress update
     * 
     * @param string $old_version Previous WordPress version
     * @param string $new_version New WordPress version
     * @return int|false The ID of the inserted log entry, or false on failure
     */
    public static function log_core_update($old_version, $new_version) {
        return self::add_log(
            self::LOG_TYPE_CORE_UPDATE,
            sprintf('WordPress core updated from %s to %s', $old_version, $new_version),
            self::STATUS_INFO,
            null,
            null,
            array(
                'old_version' => $old_version,
                'new_version' => $new_version
            )
        );
    }
} 