<?php
/**
 * CQtrLoginSecurity
 *
 * Subscribes to wp_login and wp_login_failed and raises alarms for admin logins.
 *
 * Usage (e.g. in your plugin bootstrap):
 *   add_action('plugins_loaded', function () {
 *       CQtrLoginSecurity::init();
 *   });
 */

require_once('qtrAlarmsManager.php');
require_once('qtrAlarmTypes.php');
require_once('qtrLogger.php');

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

class CQtrLoginSecurity
{
    /** @var bool */
    private static $initialized = false;

    const ACTIVITY_OPTION_KEY = 'qtr_login_activity';
    const MAX_ACTIVITY_RECORDS = 20;

    /**
     * Initialize hooks (idempotent).
     */
    public static function init(): void
    {
        if (self::$initialized) {
            return;
        }
        self::$initialized = true;
        add_action('wp_login_failed', [__CLASS__, 'on_login_failed'], 10, 1);
        add_action('wp_login',        [__CLASS__, 'on_login_success'], 10, 2);
        
        // Register scheduled events
        self::register_schedules();
    }

    /**
     * Register WordPress scheduled events for login failure monitoring.
     */
    private static function register_schedules(): void
    {
        // Hourly event to check for warning/critical thresholds
        if (!wp_next_scheduled('qtr_hourly_login_check')) {
            wp_schedule_event(time(), 'hourly', 'qtr_hourly_login_check');
        }

        // Daily event to send summary (if no warning/critical sent)
        if (!wp_next_scheduled('qtr_daily_login_summary')) {
            wp_schedule_event(time(), 'daily', 'qtr_daily_login_summary');
        }

        // Hook the scheduled events
        add_action('qtr_hourly_login_check', [__CLASS__, 'hourly_check_failures']);
        add_action('qtr_daily_login_summary', [__CLASS__, 'daily_summary_failures']);
    }

    /**
     * Handle failed login attempts.
     * 
     * Only increments counters here. Alarms are sent by scheduled events.
     *
     * @param string $username The submitted username.
     */
    public static function on_login_failed($username): void
    {
        $logger = new CQtrLogger();
        $logger->Error("Login failed for username: " . (is_string($username) ? $username : 'unknown'));

        $username = is_string($username) ? trim($username) : '';
        if ($username === '') {
            return;
        }

        // Try to resolve user; if user doesn't exist, we can't check roles reliably.
        $user = get_user_by('login', $username);
        if (!($user instanceof WP_User)) {
            return; // optionally: log unknown usernames separately
        }

        if (!self::is_admin_user($user)) {
            return;
        }

        $ip = self::get_client_ip();

        // Track activity
        self::add_activity($user->user_login, $ip, 'Failed');

        // Increment hourly counter for this username
        self::increment_hourly_counter($user->user_login);
        
        $hourly_count = self::get_hourly_count($user->user_login);
        $logger->Info('Login failure incremented for user=' . $user->user_login . ', hourly_count=' . $hourly_count);
        
        // Alarms are sent by scheduled events (hourly and daily), not here
    }

    /**
     * Handle successful logins.
     *
     * @param string  $user_login
     * @param WP_User $user
     */
    public static function on_login_success($user_login, $user): void
    {
        $logger = new CQtrLogger();
        $logger->Info("Login succeeded for username: " . (is_string($user_login) ? $user_login : 'unknown'));
        

        if (!($user instanceof WP_User)) {
            return;
        }

        if (!self::is_admin_user($user)) {
            return;
        }

        $ip = self::get_client_ip();

        // Track activity
        self::add_activity($user->user_login, $ip, 'Success');

        // Store last login timestamp for status tracking
        update_user_meta($user->ID, 'qtr_last_login', current_time('mysql'));

        // Disable email for successful logins to reduce notification noise.
        // Audit trail is still maintained for security review.
        CQtrAlarmsManager::raise(
            CQtrAlarmTypes::TYPE_LOGIN_SUCCESS,
            ['username' => $user->user_login, 'ip' => $ip, 'timestamp' => current_time('mysql'), 'site_name' => get_bloginfo('name')],
            true
        );
    }

    /**
     * Returns true if user is an administrator (capability-based, not role-string based).
     */
    private static function is_admin_user(WP_User $user): bool
    {
        // Capability check is more reliable than role name comparisons.
        return user_can($user, 'manage_options');
    }

    /**
     * Add login activity record.
     *
     * @param string $user    Username
     * @param string $ip      Client IP
     * @param string $result  'Success', 'Failed', or 'Blocked'
     */
    private static function add_activity(string $user, string $ip, string $result): void
    {
        $activity = [
            'user'   => sanitize_text_field($user),
            'ip'     => sanitize_text_field($ip),
            'time'   => current_time('mysql', true),
            'result' => in_array($result, ['Success', 'Failed', 'Blocked'], true) ? $result : 'Unknown',
        ];

        $log = get_option(self::ACTIVITY_OPTION_KEY, []);
        if (!is_array($log)) {
            $log = [];
        }

        // Add new activity at the beginning
        array_unshift($log, $activity);

        // Keep only last 20 records
        if (count($log) > self::MAX_ACTIVITY_RECORDS) {
            $log = array_slice($log, 0, self::MAX_ACTIVITY_RECORDS);
        }

        update_option(self::ACTIVITY_OPTION_KEY, $log);
    }

    /**
     * Get formatted login activity list.
     *
     * Returns array of login activities with human-readable timestamps.
     *
     * @param int $limit Maximum records to return (default: 20)
     * @return array Array of activity records formatted as requested
     *
     * @example
     * [
     *     ['user' => 'admin', 'ip' => '192.168.1.5',  'time' => '15 mins ago', 'result' => 'Success'],
     *     ['user' => 'admin', 'ip' => '45.76.23.11', 'time' => '1 hour ago',  'result' => 'Failed'],
     *     ['user' => 'john',  'ip' => '45.76.23.11', 'time' => '3 hours ago', 'result' => 'Blocked'],
     * ]
     */
    public static function get_activity(int $limit = 20): array
    {
        $limit = max(1, min(100, $limit));
        $log = get_option(self::ACTIVITY_OPTION_KEY, []);
        
        if (!is_array($log)) {
            return [];
        }

        $now = current_time('timestamp', true);
        $formatted = [];

        foreach ($log as $i => $entry) {
            if ($i >= $limit) {
                break;
            }

            if (!is_array($entry) || !isset($entry['user'], $entry['ip'], $entry['time'], $entry['result'])) {
                continue;
            }

            $timestamp = strtotime($entry['time']);
            $human_time = ($timestamp !== false) ? human_time_diff($timestamp, $now) . ' ago' : 'unknown';

            $formatted[] = [
                'user'   => (string)$entry['user'],
                'ip'     => (string)$entry['ip'],
                'time'   => $human_time,
                'result' => (string)$entry['result'],
            ];
        }

        return $formatted;
    }

    /**
     * Clear all login activity history.
     */
    public static function clear_activity(): void
    {
        delete_option(self::ACTIVITY_OPTION_KEY);
    }

    /**
     * Best-effort client IP (do not fully trust for security decisions without proxy validation).
     */
    private static function get_client_ip(): string
    {
        // Use the trusted proxy-aware IP resolution from CQtrBruteForce
        // This respects the configured trusted proxy IP list
        require_once('qtrBruteForce.php');
        $ip = CQtrBruteForce::GetClientIP();
        return $ip !== '0.0.0.0' ? $ip : 'unknown';
    }

    /**
     * Check if a login alarm has already been sent for this username in the past 24 hours.
     * Only one of these alarm types should be sent per day per username:
     * - TYPE_LOGIN_FAILED
     * - TYPE_LOGIN_FAILED_WARNING
     * - TYPE_LOGIN_FAILED_CRITICAL
     * - TYPE_LOGIN_FAILED_SUMMARY
     *
     * @param string $username The username to check
     * @return bool True if an alarm was already sent, false if it's safe to send
     */
    private static function has_login_alarm_sent_today($username): bool
    {
        $cache_key = 'qtr_login_alarm_sent_' . md5($username);
        $sent = get_transient($cache_key);
        return !empty($sent);
    }

    /**
     * Mark that a login alarm has been sent for this username today.
     *
     * @param string $username The username
     * @return void
     */
    private static function mark_login_alarm_sent($username): void
    {
        $cache_key = 'qtr_login_alarm_sent_' . md5($username);
        // Set for 24 hours (86400 seconds)
        set_transient($cache_key, 1, DAY_IN_SECONDS);
    }

    /**
     * =========================================================================
     * COUNTER METHODS - Track failed login attempts per hour
     * =========================================================================
     */

    /**
     * Increment the hourly failure counter for a username.
     * Counter is stored with current hour key, auto-expires in 1 hour.
     *
     * @param string $username The username
     * @return void
     */
    private static function increment_hourly_counter($username): void
    {
        $hour_key = self::get_hourly_counter_key($username);
        $count = (int) get_transient($hour_key);
        $count++;
        set_transient($hour_key, $count, HOUR_IN_SECONDS);
    }

    /**
     * Get the number of failures in the current hour for a username.
     *
     * @param string $username The username
     * @return int Number of failures this hour
     */
    private static function get_hourly_count($username): int
    {
        $hour_key = self::get_hourly_counter_key($username);
        return (int) get_transient($hour_key);
    }

    /**
     * Get total failures in the past 24 hours by summing last 24 hourly counters.
     *
     * @param string $username The username
     * @return int Total failures in past 24 hours
     */
    private static function get_24h_total($username): int
    {
        $total = 0;
        $now = time();
        
        // Sum counters for the past 24 hours (24 hourly buckets)
        for ($i = 0; $i < 24; $i++) {
            $timestamp = $now - ($i * HOUR_IN_SECONDS);
            $hour_key = self::get_hourly_counter_key($username, $timestamp);
            $count = (int) get_transient($hour_key);
            $total += $count;
        }
        
        return $total;
    }

    /**
     * Generate a transient key for hourly counter.
     *
     * @param string $username The username
     * @param int|null $timestamp Optional timestamp to get key for past hour
     * @return string Transient key
     */
    private static function get_hourly_counter_key($username, $timestamp = null): string
    {
        if ($timestamp === null) {
            $timestamp = time();
        }
        
        // Format: qtr_login_fails_{username}_{YYYY}{MM}{DD}{HH}
        $hour_string = gmdate('YmdH', $timestamp);
        return 'qtr_login_fails_' . md5($username) . '_' . $hour_string;
    }

    /**
     * =========================================================================
     * ALARM TYPE TRACKING - Prevent sending same type twice per day
     * =========================================================================
     */

    /**
     * Check if a specific alarm type has been sent for this username today.
     *
     * @param string $username The username
     * @param string $alarm_type One of: warning, critical, summary
     * @return bool True if alarm type was sent, false otherwise
     */
    private static function has_alarm_type_sent_today($username, $alarm_type): bool
    {
        $cache_key = 'qtr_login_alarm_' . md5($username) . '_' . $alarm_type;
        return !empty(get_transient($cache_key));
    }

    /**
     * Mark that a specific alarm type has been sent for this username today.
     *
     * @param string $username The username
     * @param string $alarm_type One of: warning, critical, summary
     * @return void
     */
    private static function mark_alarm_type_sent_today($username, $alarm_type): void
    {
        $cache_key = 'qtr_login_alarm_' . md5($username) . '_' . $alarm_type;
        set_transient($cache_key, 1, DAY_IN_SECONDS);
    }

    /**
     * Check if any high-priority alarm (critical or warning) was sent today.
     * Used to block summary emails.
     *
     * @param string $username The username
     * @return bool True if warning or critical was sent
     */
    private static function has_priority_alarm_sent_today($username): bool
    {
        return self::has_alarm_type_sent_today($username, 'critical') || 
               self::has_alarm_type_sent_today($username, 'warning');
    }

    /**
     * =========================================================================
     * SCHEDULED EVENT HANDLERS
     * =========================================================================
     */

    /**
     * Hourly event: Check for warning/critical thresholds.
     * 
     * This runs every hour and checks if any admin user reached:
     * - 100+ failures in current hour → Send CRITICAL
     * - 50-99 failures in current hour → Send WARNING
     * 
     * @return void
     */
    public static function hourly_check_failures(): void
    {
        $logger = new CQtrLogger();
        $logger->Info('Hourly login failure check started');

        // Get all admin users
        $admin_users = get_users(['role' => 'administrator']);

        foreach ($admin_users as $user) {
            $username = $user->user_login;
            $hourly_count = self::get_hourly_count($username);

            if ($hourly_count < 10) {
                // Not enough failures to trigger any alarm
                continue;
            }

            $logger->Info('Checking failures for user=' . $username . ', count=' . $hourly_count);

            // CRITICAL: 100+ failures in current hour
            if ($hourly_count >= 100) {
                if (!self::has_alarm_type_sent_today($username, 'critical')) {
                    $logger->Warning('CRITICAL threshold reached for ' . $username . ': ' . $hourly_count . ' failures');
                    
                    CQtrAlarmsManager::raise(
                        CQtrAlarmTypes::TYPE_LOGIN_FAILED_CRITICAL,
                        [
                            'username' => $username,
                            'failure_count' => $hourly_count,
                            'attack_duration' => '1 hour',
                            'affected_ips' => 'Multiple',
                            'timestamp' => current_time('mysql')
                        ],
                        true
                    );
                    
                    self::mark_alarm_type_sent_today($username, 'critical');
                    $logger->Info('CRITICAL alarm sent for ' . $username);
                } else {
                    $logger->Info('CRITICAL alarm already sent today for ' . $username . ', skipping');
                }
            }
            // WARNING: 50-99 failures in current hour (only if no critical sent)
            elseif ($hourly_count >= 50) {
                if (!self::has_alarm_type_sent_today($username, 'warning') && 
                    !self::has_alarm_type_sent_today($username, 'critical')) {
                    $logger->Warning('WARNING threshold reached for ' . $username . ': ' . $hourly_count . ' failures');
                    
                    CQtrAlarmsManager::raise(
                        CQtrAlarmTypes::TYPE_LOGIN_FAILED_WARNING,
                        [
                            'username' => $username,
                            'failure_count' => $hourly_count,
                            'affected_ips' => 'Multiple',
                            'timestamp' => current_time('mysql')
                        ],
                        true
                    );
                    
                    self::mark_alarm_type_sent_today($username, 'warning');
                    $logger->Info('WARNING alarm sent for ' . $username);
                } else {
                    $logger->Info('Warning/Critical alarm already sent today for ' . $username . ', skipping');
                }
            }
        }

        $logger->Info('Hourly login failure check completed');
    }

    /**
     * Daily event: Send summary if no warning/critical was sent.
     * 
     * This runs once per day and checks if any admin user had:
     * - 10+ failures in past 24 hours AND no warning/critical sent → Send SUMMARY
     * 
     * @return void
     */
    public static function daily_summary_failures(): void
    {
        $logger = new CQtrLogger();
        $logger->Debug('Daily login failure summary check started');

        // Get all admin users
        $admin_users = get_users(['role' => 'administrator']);

        foreach ($admin_users as $user) {
            $username = $user->user_login;
            
            // Skip if warning or critical was already sent today (priority alarms block summary)
            if (self::has_priority_alarm_sent_today($username)) {
                $logger->Info('Priority alarm already sent for ' . $username . ', skipping summary');
                continue;
            }

            $total_24h = self::get_24h_total($username);

            if ($total_24h >= 10) {
                if (!self::has_alarm_type_sent_today($username, 'summary')) {
                    $logger->Info('SUMMARY threshold reached for ' . $username . ': ' . $total_24h . ' failures in 24h');
                    
                    CQtrAlarmsManager::raise(
                        CQtrAlarmTypes::TYPE_LOGIN_FAILED_SUMMARY,
                        [
                            'username' => $username,
                            'total_failures' => $total_24h,
                            'affected_users' => $username,
                            'unique_ips' => 'Multiple',
                            'period' => 'Last 24 hours',
                            'site_name' => get_bloginfo('name'),
                            'timestamp' => current_time('mysql')
                        ],
                        true
                    );
                    
                    self::mark_alarm_type_sent_today($username, 'summary');
                    $logger->Info('SUMMARY alarm sent for ' . $username);
                } else {
                    $logger->Info('SUMMARY alarm already sent today for ' . $username . ', skipping');
                }
            } else {
                $logger->Info('Insufficient failures for summary: ' . $username . ' (' . $total_24h . ' < 10)');
            }
        }

        $logger->Info('Daily login failure summary check completed');
    }

    /**
     * Best-effort user agent.
     */
    private static function get_user_agent(): string
    {
        $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
        $ua = is_string($ua) ? trim($ua) : '';
        return $ua !== '' ? $ua : 'unknown';
    }

    /**
     * =========================================================================
     * CLEANUP HOOKS - Remove transients on deactivation/uninstall
     * =========================================================================
     */

    /**
     * Clean up all login security transients.
     * Called on plugin deactivation or uninstall.
     * 
     * Removes:
     * - Hourly counter buckets (last 48 hours)
     * - Guard transients (warning, critical, summary per username)
     *
     * @return void
     */
    public static function cleanup_transients(): void
    {
        $logger = new CQtrLogger();
        $logger->Info('Starting login security transients cleanup');

        // Get all admin users
        $admin_users = get_users(['role' => 'administrator']);

        if (empty($admin_users)) {
            $logger->Info('No admin users found, cleanup completed');
            return;
        }

        $users_cleaned = 0;

        foreach ($admin_users as $user) {
            $username = $user->user_login;
            
            // Delete hourly counter buckets (last 48 hours to be safe)
            self::delete_hourly_counters($username);
            
            // Delete guard transients
            self::delete_guard_transients($username);
            
            $users_cleaned++;
            $logger->Info('Cleaned transients for user: ' . $username);
        }

        $logger->Info('Login security transients cleanup completed. Users cleaned: ' . $users_cleaned);
    }

    /**
     * Delete hourly counter transients for a username.
     * Cleans up last 48 hours of counters to ensure complete cleanup.
     *
     * @param string $username The username
     * @return void
     */
    private static function delete_hourly_counters($username): void
    {
        $now = time();
        $deleted_count = 0;
        
        // Delete last 48 hours of hourly buckets (to cover any edge cases)
        for ($i = 0; $i < 48; $i++) {
            $timestamp = $now - ($i * HOUR_IN_SECONDS);
            $hour_key = self::get_hourly_counter_key($username, $timestamp);
            
            if (delete_transient($hour_key)) {
                $deleted_count++;
            }
        }

        if ($deleted_count > 0) {
            $logger = new CQtrLogger();
            $logger->Debug('Deleted ' . $deleted_count . ' hourly counter transients for user: ' . $username);
        }
    }

    /**
     * Delete all guard transients for a username.
     * Removes transients that track whether each alarm type has been sent.
     *
     * Guard types:
     * - warning: TYPE_LOGIN_FAILED_WARNING sent today
     * - critical: TYPE_LOGIN_FAILED_CRITICAL sent today
     * - summary: TYPE_LOGIN_FAILED_SUMMARY sent today
     *
     * @param string $username The username
     * @return void
     */
    private static function delete_guard_transients($username): void
    {
        $types = array('warning', 'critical', 'summary');
        $deleted_count = 0;
        
        foreach ($types as $type) {
            $cache_key = 'qtr_login_alarm_' . md5($username) . '_' . $type;
            
            if (delete_transient($cache_key)) {
                $deleted_count++;
            }
        }

        if ($deleted_count > 0) {
            $logger = new CQtrLogger();
            $logger->Debug('Deleted ' . $deleted_count . ' guard transients for user: ' . $username);
        }
    }
}

?>