<?php

/**
 * Sentinel Auth Counters Class
 *
 * Handles real-time authentication attempt tracking using WordPress transients/object cache.
 * Provides ephemeral counters for brute force detection with automatic TTL cleanup.
 */

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

class Sentinel_Auth_Counters
{

    /**
     * Cache TTL in seconds (15 minutes)
     */
    const CACHE_TTL = 900;

    /**
     * Cache key prefix
     */
    const CACHE_PREFIX = 'sentinel:auth:';

    /**
     * Maximum usernames to track per IP (prevent memory bloat)
     */
    const MAX_USERNAMES_TRACKED = 50;

    /**
     * Capture and process an authentication attempt
     *
     * @param array $ctx Context array with ip, username, result, source, etc.
     * @return void
     */
    public static function capture_attempt($ctx)
    {
        // Validate required context
        if (empty($ctx['ip']) || empty($ctx['result']) || empty($ctx['source'])) {
            return;
        }

        $ip = $ctx['ip'];
        $username = $ctx['username'] ?? null;
        $source = $ctx['source'];
        $is_fail = ($ctx['result'] === 'fail');

        if (self::is_ip_blocked($ip)) {
            // Log the blocked attempt
            if (function_exists('sentinel_log_event')) {
                sentinel_log_event('security_blocked_attempt', array(
                    'ip' => $ip,
                    'username' => $username,
                    'source' => $source,
                    'block_info' => self::get_ip_block_info($ip)
                ));
            }

            // Immediately exit with error - don't process further
            wp_die(
                __('Access temporarily restricted due to suspicious activity. Please try again later.', 'sentinel'),
                __('Access Restricted', 'sentinel'),
                array('response' => 403)
            );
        }

        // Skip if IP is allowlisted (but still log raw event)
        if (self::is_ip_allowlisted($ip)) {
            return;
        }

        // Skip if IP is currently trusted (admin logged in recently)
        if (self::is_ip_trusted($ip)) {
            return;
        }

        // Increment counters and get current snapshot
        $snapshot = self::increment_counters($ip, $username, $source, $is_fail);

        // Add snapshot data to context for logging
        $ctx['counters_snapshot'] = $snapshot;

        // Evaluate snapshot for incident detection (Sentinel+ feature)
        if ($is_fail && function_exists('sentinel_incident_evaluate')) {
            $incident_result = sentinel_incident_evaluate($snapshot, $ctx);
            if ($incident_result) {
                $ctx['incident_triggered'] = $incident_result;

                // ACTION ENGINE: Take action based on incident detection
                self::execute_security_action($incident_result, $ctx);
            }
        }

        // Log the raw authentication event via existing Sentinel system
        if (function_exists('sentinel_log_event')) {
            sentinel_log_event('auth_' . $ctx['result'], array(
                'username' => $username,
                'source' => $source,
                'user_agent' => $ctx['ua'] ?? '',
                'url' => $ctx['url'] ?? '',
                'snapshot' => $snapshot,
                'incident_triggered' => isset($ctx['incident_triggered']) ? $ctx['incident_triggered'] : null,
                'allowlisted' => false
            ));
        }
    }

    /**
     * Increment counters for an IP and return current snapshot
     *
     * @param string $ip IP address
     * @param string|null $username Username attempted (if any)
     * @param string $source Source of attempt (wp-login, xmlrpc, etc.)
     * @param bool $is_fail Whether this was a failed attempt
     * @return array Current counter snapshot
     */
    public static function increment_counters($ip, $username = null, $source = 'wp-login', $is_fail = false)
    {
        $current_time = time();

        // Get or initialize main IP counter
        $ip_key = self::CACHE_PREFIX . 'ip:' . $ip;
        $ip_data = wp_cache_get($ip_key) ?: self::get_transient_fallback($ip_key);

        if (!$ip_data) {
            $ip_data = array(
                'fails' => 0,
                'last_ts' => $current_time,
                'usernames_count' => 0,
                'xmlrpc_count' => 0,
                'window_start' => $current_time
            );
        }

        // Update basic counters
        if ($is_fail) {
            $ip_data['fails']++;
        }
        $ip_data['last_ts'] = $current_time;

        // Handle username enumeration tracking
        if ($username && $is_fail) {
            $username_data = self::update_username_tracking($ip, $username);
            $ip_data['usernames_count'] = $username_data['unique_count'];
        }

        // Handle XML-RPC specific tracking
        if ($source === 'xmlrpc') {
            $ip_data['xmlrpc_count']++;
            self::update_source_counter($ip, $source, $is_fail);
        }

        // Save updated IP counter
        self::set_cache_with_fallback($ip_key, $ip_data);

        // Return current snapshot for rule evaluation
        return array(
            'fails' => $ip_data['fails'],
            'distinct_usernames' => $ip_data['usernames_count'],
            'xmlrpc_count' => $ip_data['xmlrpc_count'],
            'last_ts' => $ip_data['last_ts'],
            'window_start' => $ip_data['window_start'],
            'source' => $source
        );
    }

    /**
     * Update username enumeration tracking for an IP
     *
     * @param string $ip IP address
     * @param string $username Username to track
     * @return array Username tracking data
     */
    private static function update_username_tracking($ip, $username)
    {
        $username_key = self::CACHE_PREFIX . 'ip_usernames:' . $ip;
        $username_data = wp_cache_get($username_key) ?: self::get_transient_fallback($username_key);

        if (!$username_data) {
            $username_data = array(
                'usernames' => array(),
                'unique_count' => 0,
                'last_updated' => time()
            );
        }

        // Add username if not already tracked
        if (!in_array($username, $username_data['usernames'])) {
            $username_data['usernames'][] = $username;

            // Keep only the most recent usernames to prevent memory bloat
            if (count($username_data['usernames']) > self::MAX_USERNAMES_TRACKED) {
                $username_data['usernames'] = array_slice($username_data['usernames'], -self::MAX_USERNAMES_TRACKED);
            }
        }

        $username_data['unique_count'] = count($username_data['usernames']);
        $username_data['last_updated'] = time();

        self::set_cache_with_fallback($username_key, $username_data);

        return $username_data;
    }

    /**
     * Update source-specific counter (e.g., XML-RPC)
     *
     * @param string $ip IP address
     * @param string $source Source identifier
     * @param bool $is_fail Whether this was a failure
     */
    private static function update_source_counter($ip, $source, $is_fail)
    {
        $source_key = self::CACHE_PREFIX . 'ip_src:' . $ip . ':' . $source;
        $source_data = wp_cache_get($source_key) ?: self::get_transient_fallback($source_key);

        if (!$source_data) {
            $source_data = array(
                'fails' => 0,
                'total' => 0,
                'last_ts' => time(),
                'window_start' => time()
            );
        }

        if ($is_fail) {
            $source_data['fails']++;
        }
        $source_data['total']++;
        $source_data['last_ts'] = time();

        self::set_cache_with_fallback($source_key, $source_data);
    }

    /**
     * Get current counter snapshot for an IP (without incrementing)
     *
     * @param string $ip IP address
     * @return array|null Counter snapshot or null if no data
     */
    public static function get_counter_snapshot($ip)
    {
        $ip_key = self::CACHE_PREFIX . 'ip:' . $ip;
        $ip_data = wp_cache_get($ip_key) ?: self::get_transient_fallback($ip_key);

        if (!$ip_data) {
            return null;
        }

        return array(
            'fails' => $ip_data['fails'],
            'distinct_usernames' => $ip_data['usernames_count'],
            'xmlrpc_count' => $ip_data['xmlrpc_count'],
            'last_ts' => $ip_data['last_ts'],
            'window_start' => $ip_data['window_start']
        );
    }

    /**
     * Clear all counters for an IP (used for allowlisting/resolution)
     *
     * @param string $ip IP address
     */
    public static function clear_ip_counters($ip)
    {
        $keys = array(
            self::CACHE_PREFIX . 'ip:' . $ip,
            self::CACHE_PREFIX . 'ip_usernames:' . $ip
        );

        // Also clear source-specific keys (we'll try common ones)
        $sources = array('wp-login', 'xmlrpc', 'rest', 'woo');
        foreach ($sources as $source) {
            $keys[] = self::CACHE_PREFIX . 'ip_src:' . $ip . ':' . $source;
        }

        foreach ($keys as $key) {
            wp_cache_delete($key);
            delete_transient(str_replace(':', '_', $key)); // Fallback cleanup
        }
    }

    /**
     * Check if IP is allowlisted
     *
     * @param string $ip IP address
     * @return bool
     */
    public static function is_ip_allowlisted($ip)
    {
        $security_settings = get_option('sentinel_security_settings', array());
        $allowlist = $security_settings['ip_allowlist'] ?? '';

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

        // Parse allowlist (CSV or line-separated)
        $allowed_ips = array_map('trim', preg_split('/[,\r\n]+/', $allowlist, -1, PREG_SPLIT_NO_EMPTY));

        foreach ($allowed_ips as $allowed_ip) {
            // Check for exact match first
            if ($ip === $allowed_ip) {
                return true;
            }

            // Check for CIDR notation (e.g., 192.168.1.0/24)
            if (strpos($allowed_ip, '/') !== false) {
                list($subnet, $bits) = explode('/', $allowed_ip, 2);
                $bits = intval($bits);

                // Validate CIDR format
                if ($bits >= 0 && $bits <= 32 && filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
                    $subnet_long = ip2long($subnet);
                    $ip_long = ip2long($ip);
                    $mask = -1 << (32 - $bits);

                    if (($ip_long & $mask) === ($subnet_long & $mask)) {
                        return true;
                    }
                }
            }
        }

        return false;
    }

    /**
     * Check if IP is currently trusted (admin recently logged in)
     *
     * @param string $ip IP address
     * @return bool
     */
    public static function is_ip_trusted($ip)
    {
        $trust_key = self::CACHE_PREFIX . 'trust:' . $ip;
        $trust_data = wp_cache_get($trust_key) ?: self::get_transient_fallback($trust_key);

        return !empty($trust_data) && $trust_data['expires'] > time();
    }

    /**
     * Mark an IP as trusted for a duration
     *
     * @param string $ip IP address
     * @param int $duration Trust duration in seconds
     */
    public static function mark_ip_trusted($ip, $duration = 86400)
    {
        $trust_key = self::CACHE_PREFIX . 'trust:' . $ip;
        $trust_data = array(
            'created' => time(),
            'expires' => time() + $duration
        );

        self::set_cache_with_fallback($trust_key, $trust_data, $duration);
    }

    /**
     * Set cache with transient fallback
     *
     * @param string $key Cache key
     * @param mixed $value Value to store
     * @param int $ttl TTL in seconds (default: CACHE_TTL)
     */
    private static function set_cache_with_fallback($key, $value, $ttl = null)
    {
        $ttl = $ttl ?: self::CACHE_TTL;

        // Try object cache first
        if (!wp_cache_set($key, $value, '', $ttl)) {
            // Fallback to transients (replace colons with underscores for transient keys)
            $transient_key = str_replace(':', '_', $key);
            set_transient($transient_key, $value, $ttl);
        }
    }

    /**
     * Get transient fallback when object cache fails
     *
     * @param string $key Original cache key
     * @return mixed|false
     */
    private static function get_transient_fallback($key)
    {
        $transient_key = str_replace(':', '_', $key);
        return get_transient($transient_key);
    }

    /**
     * Get statistics about current counters (for debugging/admin)
     *
     * @return array Counter statistics
     */
    public static function get_counter_stats()
    {
        global $wpdb;

        // Simple implementation for monitoring active counters
        return array(
            'message' => 'Counter stats require active monitoring - use incident records for historical data'
        );
    }

    /**
     * Clean up expired counters manually (optional maintenance)
     * This is handled automatically by transient/cache expiration, but can be called for cleanup
     */
    public static function cleanup_expired_counters()
    {
        // Object cache and transients handle TTL automatically
        // This method exists for potential future manual cleanup needs
        return true;
    }

    /**
     * ACTION ENGINE: Execute security action based on incident detection
     *
     * This is the "muscle" that actually takes action when threats are detected.
     * Based on the action_mode setting, it can:
     * - observe: Just log (do nothing)
     * - throttle: Add delay to slow down attacks
     * - block: Temporarily block the IP address
     *
     * @param array $incident_data Incident data from evaluation
     * @param array $ctx Authentication context (IP, username, etc.)
     */
    private static function execute_security_action($incident_data, $ctx)
    {
        // Get security settings
        $security_settings = get_option('sentinel_security_settings', array());
        $action_mode = $security_settings['action_mode'] ?? 'observe';

        $ip = $ctx['ip'];
        $incident_type = $incident_data['type'] ?? 'unknown';

        // Log what action we're about to take
        if (function_exists('sentinel_log_event')) {
            sentinel_log_event('security_action_triggered', array(
                'ip' => $ip,
                'action_mode' => $action_mode,
                'incident_type' => $incident_type,
                'username' => $ctx['username'] ?? null,
                'source' => $ctx['source'] ?? 'unknown'
            ));
        }

        switch ($action_mode) {
            case 'observe':
                // Do nothing - just observe and log
                break;

            case 'throttle':
                // Add delay to slow down the attacker
                self::throttle_request($ip, $security_settings, $ctx);
                break;

            case 'block':
                // Temporarily block the IP address
                self::block_ip_temporarily($ip, $security_settings, $ctx);
                break;

            default:
                // Unknown mode - default to observe
                break;
        }
    }

    /**
     * THROTTLE: Add delay to slow down suspicious requests
     *
     * When throttling mode is active, we add a configurable delay to
     * authentication requests to slow down brute force attacks while
     * still allowing legitimate users to eventually get through.
     *
     * @param string $ip IP address to throttle
     * @param array $settings Security settings
     * @param array $ctx Request context
     */
    private static function throttle_request($ip, $settings, $ctx)
    {
        $delay_seconds = $settings['throttle_delay'] ?? 3;

        // Ensure delay is within reasonable bounds (1-10 seconds)
        $delay_seconds = max(1, min(10, intval($delay_seconds)));

        // Log the throttling action
        if (function_exists('sentinel_log_event')) {
            sentinel_log_event('security_throttle_applied', array(
                'ip' => $ip,
                'delay_seconds' => $delay_seconds,
                'username' => $ctx['username'] ?? null,
                'source' => $ctx['source'] ?? 'unknown'
            ));
        }

        // Apply the delay
        sleep($delay_seconds);
    }

    /**
     * BLOCK: Temporarily block IP address from authentication
     *
     * When blocking mode is active, we store a temporary "block" flag
     * for the IP address. Future authentication attempts will be rejected
     * immediately without processing until the block expires.
     *
     * @param string $ip IP address to block
     * @param array $settings Security settings
     * @param array $ctx Request context
     */
    private static function block_ip_temporarily($ip, $settings, $ctx)
    {
        $block_duration = $settings['temp_block_duration'] ?? 3600; // Default 1 hour

        // Ensure duration is within bounds (5 minutes to 24 hours)
        $block_duration = max(300, min(86400, intval($block_duration)));

        // Create block record using WordPress transients
        $block_key = 'sentinel_blocked_ip_' . md5($ip);
        $block_data = array(
            'ip' => $ip,
            'blocked_at' => current_time('mysql'),
            'expires_at' => date('Y-m-d H:i:s', time() + $block_duration),
            'reason' => 'security_incident',
            'incident_type' => $ctx['incident_triggered']['type'] ?? 'unknown',
            'source' => $ctx['source'] ?? 'unknown'
        );

        // Store the block using WordPress transients (auto-expires)
        set_transient($block_key, $block_data, $block_duration);

        // Log the blocking action
        if (function_exists('sentinel_log_event')) {
            sentinel_log_event('security_ip_blocked', array(
                'ip' => $ip,
                'duration_seconds' => $block_duration,
                'duration_hours' => round($block_duration / 3600, 1),
                'expires_at' => $block_data['expires_at'],
                'username' => $ctx['username'] ?? null,
                'source' => $ctx['source'] ?? 'unknown',
                'incident_type' => $block_data['incident_type']
            ));
        }
    }

    /**
     * CHECK: Is IP address currently blocked?
     *
     * Before processing any authentication attempt, check if the IP
     * is currently in a temporary block state. If so, reject immediately.
     *
     * @param string $ip IP address to check
     * @return bool True if IP is currently blocked
     */
    public static function is_ip_blocked($ip)
    {
        $block_key = 'sentinel_blocked_ip_' . md5($ip);
        $block_data = get_transient($block_key);

        return ($block_data !== false);
    }

    /**
     * GET: Get block information for IP address
     *
     * Returns details about an IP block for admin display or debugging.
     *
     * @param string $ip IP address to check
     * @return array|null Block data or null if not blocked
     */
    public static function get_ip_block_info($ip)
    {
        $block_key = 'sentinel_blocked_ip_' . md5($ip);
        $block_data = get_transient($block_key);

        return ($block_data !== false) ? $block_data : null;
    }
}
