<?php
if (!defined('ABSPATH')) exit;

require_once('qtrLogger.php');
require_once('qtrSettings.php');
require_once('qtrAlarmTypes.php');
require_once('qtrAlarmFactory.php');

class CQtrAlarmsManager
{
    const TABLE_SUFFIX = 'qtr_alarms';

    
    public static function table_name()
    {
        global $wpdb;
        return $wpdb->prefix . self::TABLE_SUFFIX;
    }

    public static function install()
    {
        global $wpdb;
        $table = self::table_name();
        $charset_collate = $wpdb->get_charset_collate();

        // Check if table already exists
        $table_exists = $wpdb->get_var(
            $wpdb->prepare("SHOW TABLES LIKE %s", $table)
        ) === $table;

        if (!$table_exists) {
            require_once ABSPATH . 'wp-admin/includes/upgrade.php';

            $sql = "CREATE TABLE {$table} (
                id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
                created_at DATETIME NOT NULL,
                severity VARCHAR(16) NOT NULL,
                body LONGTEXT NOT NULL,
                ack TINYINT(1) NOT NULL DEFAULT 0,
                ack_at DATETIME NULL,
                ack_by BIGINT(20) UNSIGNED NULL,
                PRIMARY KEY  (id),
                KEY created_at (created_at),
                KEY severity (severity),
                KEY ack (ack)
            ) {$charset_collate};";

            dbDelta($sql);
        }

        // Settings are managed through QutteraSettings class
        // No need to manually manage notification options in database
    }

    private static function normalize_severity($severity)
    {
        $severity = strtolower(trim((string)$severity));
        $allowed = array('info', 'warning', 'error', 'critical');
        return in_array($severity, $allowed, true) ? $severity : 'info';
    }

    /**
     * Get default rate-limit configuration for alarm types (minutes between emails)
     * 
     * Critical alarms (0) are never rate-limited.
     * Other alarms respect per-type cooldown to prevent notification flooding.
     * 
     * @return array Alarm type => cooldown minutes mapping
     */
    private static function get_default_rate_limits()
    {
        return array(
            CQtrAlarmTypes::TYPE_BRUTE_FORCE_SOFT_LOCK  => 15,
            CQtrAlarmTypes::TYPE_BRUTE_FORCE_HARD_LOCK  => 30,
            CQtrAlarmTypes::TYPE_BRUTE_FORCE_SPRAY      => 60,
            CQtrAlarmTypes::TYPE_BRUTE_FORCE_IP_LOCK    => 20,
            CQtrAlarmTypes::TYPE_BRUTE_FORCE_COMBO_LOCK => 20,
            CQtrAlarmTypes::TYPE_BRUTE_FORCE_BLACKLIST  => 30,
            CQtrAlarmTypes::TYPE_ADMIN_ACCOUNT_LOCKED   => 1440,  // Send once per 24 hours (1440 minutes)
            CQtrAlarmTypes::TYPE_LOGIN_FAILED           => 1440,  // Send once per 24 hours (1440 minutes)
            CQtrAlarmTypes::TYPE_LOGIN_FAILED_WARNING   => 1440,  // Send once per 24 hours (1440 minutes)
            CQtrAlarmTypes::TYPE_LOGIN_FAILED_CRITICAL  => 1440,  // Send once per 24 hours (1440 minutes)
            CQtrAlarmTypes::TYPE_LOGIN_FAILED_SUMMARY   => 1440,  // Send once per 24 hours (1440 minutes)
            CQtrAlarmTypes::TYPE_LOGIN_SUCCESS          => 0,
            CQtrAlarmTypes::TYPE_ADMIN_ADDED            => 0,
            CQtrAlarmTypes::TYPE_ADMIN_REMOVED          => 0,
            CQtrAlarmTypes::TYPE_ADMIN_ROLE_CHANGED     => 0,
            CQtrAlarmTypes::TYPE_BOT_BLOCK              => 10,
        );
    }

    /**
     * Compute unique signature for alarm to enable rate-limiting
     * 
     * Groups identical or very similar alarms together for rate-limiting.
     * Different alarm types have different identifying attributes.
     * 
     * @param string $alarm_type Alarm type constant
     * @param array $params Alarm parameters
     * @return string SHA256 hash signature
     */
    private static function compute_alarm_signature($alarm_type, $params)
    {
        $key_parts = array($alarm_type);

        $signature_map = array(
            CQtrAlarmTypes::TYPE_BRUTE_FORCE_SOFT_LOCK  => array('username', 'ip'),
            CQtrAlarmTypes::TYPE_BRUTE_FORCE_HARD_LOCK  => array('username', 'ip'),
            CQtrAlarmTypes::TYPE_BRUTE_FORCE_SPRAY      => array('ip'),
            CQtrAlarmTypes::TYPE_BRUTE_FORCE_IP_LOCK    => array('ip'),
            CQtrAlarmTypes::TYPE_BRUTE_FORCE_COMBO_LOCK => array('username', 'ip'),
            CQtrAlarmTypes::TYPE_BRUTE_FORCE_BLACKLIST  => array('ip'),
            CQtrAlarmTypes::TYPE_ADMIN_ACCOUNT_LOCKED   => array('username'),   // Per-username basis
            CQtrAlarmTypes::TYPE_LOGIN_FAILED           => array('username'),   // Per-username basis (one alarm per 24h)
            CQtrAlarmTypes::TYPE_LOGIN_FAILED_WARNING   => array('username'),   // Per-username basis (one alarm per 24h)
            CQtrAlarmTypes::TYPE_LOGIN_FAILED_CRITICAL  => array('username'),   // Per-username basis (one alarm per 24h)
            CQtrAlarmTypes::TYPE_LOGIN_FAILED_SUMMARY   => array('username'),   // Per-username basis (one alarm per 24h)
            CQtrAlarmTypes::TYPE_LOGIN_SUCCESS          => array('username', 'ip'),
            CQtrAlarmTypes::TYPE_BOT_BLOCK              => array('ip'),
            CQtrAlarmTypes::TYPE_ADMIN_ADDED            => array(),
            CQtrAlarmTypes::TYPE_ADMIN_REMOVED          => array(),
            CQtrAlarmTypes::TYPE_ADMIN_ROLE_CHANGED     => array(),
        );

        $identifiers = isset($signature_map[$alarm_type]) ? $signature_map[$alarm_type] : array();

        foreach ($identifiers as $key) {
            if (isset($params[$key])) {
                $key_parts[] = (string)$params[$key];
            }
        }

        return hash('sha256', implode(':', $key_parts));
    }

    /**
     * Get critical alarm types that always bypass rate-limiting
     * 
     * @return array List of critical alarm type constants
     */
    private static function get_critical_alarm_types()
    {
        return array(
            CQtrAlarmTypes::TYPE_ADMIN_ACCOUNT_LOCKED,
            CQtrAlarmTypes::TYPE_ADMIN_ADDED,
            CQtrAlarmTypes::TYPE_ADMIN_REMOVED,
            CQtrAlarmTypes::TYPE_ADMIN_ROLE_CHANGED,
            CQtrAlarmTypes::TYPE_BRUTE_FORCE_SPRAY,
            CQtrAlarmTypes::TYPE_BRUTE_FORCE_HARD_LOCK,
            CQtrAlarmTypes::TYPE_BRUTE_FORCE_BLACKLIST,
            CQtrAlarmTypes::TYPE_LOGIN_FAILED_CRITICAL,
        );
    }

    /**
     * Check if global email limit has been reached
     * 
     * Prevents email flooding during distributed attacks by limiting
     * total emails per hour (critical alarms always bypass this).
     * 
     * @return bool True if limit not reached, false if limit exceeded
     */
    private static function check_global_email_limit()
    {
        $max_emails_per_hour = 2;  // Configurable max emails per hour
        
        $transient_key = 'qtr_alarm_emails_sent_hour';
        $emails_sent = (int) get_transient($transient_key);
        
        if ($emails_sent >= $max_emails_per_hour) {
            $logger = new CQtrLogger();            
            $logger->Warning("Global email limit reached ({$emails_sent}/{$max_emails_per_hour}). Suppressing non-critical alarm email.");
            return false;
        }
        
        return true;
    }

    /**
     * Increment global email counter
     * 
     * @private
     */
    private static function increment_email_counter()
    {
        $transient_key = 'qtr_alarm_emails_sent_hour';
        $emails_sent = (int) get_transient($transient_key);
        $emails_sent++;
        set_transient($transient_key, $emails_sent, 3600);  // Reset every hour
    }

    /**
     * Check if alarm type is critical (bypasses global rate-limiting)
     * 
     * @param string $alarm_type Alarm type constant
     * @return bool True if alarm is critical
     */
    private static function is_critical_alarm($alarm_type)
    {
        return in_array($alarm_type, self::get_critical_alarm_types(), true);
    }

    /**
     * Get current global email count for the hour
     * 
     * @return int Number of emails sent in current hour
     */
    public static function get_global_email_count()
    {
        return (int) get_transient('qtr_alarm_emails_sent_hour');
    }

    /**
     * Reset global email counter (for testing or manual reset)
     * 
     * @return bool True if reset successfully
     */
    public static function reset_global_email_counter()
    {
        return delete_transient('qtr_alarm_emails_sent_hour');
    }

    /**
     * Check if email should be sent based on rate-limit status
     * 
     * Uses WordPress transients to track when each alarm signature was last emailed.
     * Prevents notification flooding for repeated identical events.
     * 
     * Critical alarms (0-minute cooldown) always return true.
     * 
     * @param string $alarm_type Alarm type constant
     * @param array $params Alarm parameters
     * @return bool True if email should be sent, false if rate-limited
     */
    private static function should_send_email($alarm_type, $params)
    {
        $logger = new CQtrLogger();

        $rate_limits = self::get_default_rate_limits();
        $cooldown_minutes = isset($rate_limits[$alarm_type]) ? (int)$rate_limits[$alarm_type] : 15;

        if ($cooldown_minutes === 0) {
            $logger->Debug("Rate-limit: ALWAYS_SEND (critical) for " . $alarm_type);
            return true;
        }

        $signature = self::compute_alarm_signature($alarm_type, $params);
        $transient_key = 'qtr_alarm_ratelimit_' . $signature;

        $last_sent = get_transient($transient_key);

        if ($last_sent === false) {
            $logger->Debug("Rate-limit: PASS (new/expired) for " . $alarm_type . " sig=" . substr($signature, 0, 8));
            set_transient($transient_key, time(), $cooldown_minutes * 60);
            return true;
        }

        $logger->Debug("Rate-limit: SKIP (cooldown " . $cooldown_minutes . "m) for " . $alarm_type . " sig=" . substr($signature, 0, 8));

        $suppressed_today = get_transient('qtr_suppressed_alarms_today');
        $suppressed_today = $suppressed_today ? (int)$suppressed_today + 1 : 1;
        set_transient('qtr_suppressed_alarms_today', $suppressed_today, 86400);

        return false;
    }

    /**
     * Raise an alarm (stores in DB + optional email).
     *
     * Factory Mode (Recommended):
     *   raise(CQtrAlarmTypes::TYPE_LOGIN_FAILED, array_of_params, true)
     *
     * @param string $alarm_type Alarm type constant from CQtrAlarmTypes
     * @param array  $params Parameters for template substitution
     * @param bool   $send_email Whether to send email notification (respects rate-limiting)
     * @param bool   $force_email If true, bypasses rate-limiting (for critical/manual overrides)
     *
     * @return int|WP_Error Inserted alarm id or WP_Error on failure
     */
    public static function raise($alarm_type, $params, $send_email = true, $force_email = false)
    {
        global $wpdb;
        
        $logger = new CQtrLogger();

        $table = self::table_name();
        $now_gmt = current_time('mysql', true);
        $now_local = get_date_from_gmt($now_gmt, 'Y-m-d H:i:s');

        // Validate alarm type is provided
        if ($alarm_type === null) {
            $logger->Error("Alarm type is required for raise()");
            return new WP_Error('missing_alarm_type', 'Alarm type not provided.');
        }
        
        // Generate alarm message from factory
        $alarm = CQtrAlarmFactory::generate($alarm_type, (array)$params);
        if (is_wp_error($alarm)) {
            $logger->Error("Failed to generate alarm: " . $alarm->get_error_message());
            return $alarm;
        }
        
        $severity = $alarm['severity'];
        $body_for_db = wp_kses_post($alarm['body']);  // Plain text version for database storage

        // Store alarm in database
        $ok = $wpdb->insert(
            $table,
            array(
                'created_at' => $now_local,
                'severity'   => $severity,
                'body'       => $body_for_db,
                'ack'        => 0,
                'ack_at'     => null,
                'ack_by'     => null,
            ),
            array('%s','%s','%s','%d','%s','%d')
        );

        if (!$ok) {
            $logger->Error("Failed to insert alarm into database.");
            return new WP_Error('qtr_alarm_insert_failed', 'Failed to insert alarm.');
        }

        $alarm_id = (int)$wpdb->insert_id;

        // Auto-cleanup: keep only 100 most recent alarms
        self::cleanup_old_alarms(100);

        // Send email notification if requested and not rate-limited
        if ($send_email) {
            $should_email = $force_email || self::should_send_email($alarm_type, $params);
            if ($should_email) {
                self::maybe_send_email($alarm_id, $alarm, $alarm_type, $now_local);
            } else {
                $logger->Debug("Alarm ID " . $alarm_id . " stored but email suppressed by rate-limit.");
            }
        }

        return $alarm_id;
    }

    /**
     * Send email for factory-generated alarms
     * 
     * Only called after rate-limit check passes.
     * Sends formatted HTML email with alarm details.
     * 
     * @private
     */
    private static function maybe_send_email($alarm_id, $alarm, $alarm_type, $created_at)
    {
        $logger = new CQtrLogger();
        $logger->Debug("Sending alarm email for alarm ID: " . $alarm_id);

        // Check global email limit for non-critical alarms
        // Alarms with 0-minute cooldown (always send) bypass global limit
        $rate_limits = self::get_default_rate_limits();
        $cooldown_minutes = isset($rate_limits[$alarm_type]) ? (int)$rate_limits[$alarm_type] : 15;
        $is_always_send = ($cooldown_minutes === 0);
        
        if (!$is_always_send && !self::is_critical_alarm($alarm_type)) {
            if (!self::check_global_email_limit()) {
                $logger->Debug("Alarm ID " . $alarm_id . " email suppressed: global hour limit reached");
                return;
            }
        }

        // Load settings from database
        $settings = QutteraSettings::LoadFromOptions();
        if (!$settings || !$settings->GetAlarmsEnabled()) {
            $logger->Error("Cannot send alarm. Alarm emails are disabled in settings.");
            return;
        }

        $to = sanitize_email($settings->GetAlertEmail());
        if (empty($to) || !is_email($to)) {
            $logger->Error("Cannot send alarm. Invalid email address configured: " . $to);
            return;
        }

        $subject = $alarm['subject'];

        // Get formatted email body from factory (includes metadata header, body, and footer)
        $email_body = CQtrAlarmFactory::format_email_body($alarm_id, $alarm, $created_at);

        // Set headers with HTML content type
        $headers = array('Content-Type: text/html; charset=UTF-8');

        $logger->Info("Sending alarm email for alarm ID: " . $alarm_id . " to " . $to);

        // Send email and log result, this call can fail if phpmailer is not configured properly
        if (wp_mail($to, $subject, $email_body, $headers)) {
            $logger->Debug("Alarm email sent successfully for alarm ID: " . $alarm_id . " | Subject: " . $subject);
            // Increment global email counter
            self::increment_email_counter();
        } else {
            global $phpmailer;
            $logger->Info("Failed to send alarm email for alarm ID: " . $alarm_id);
            $error = isset($phpmailer->ErrorInfo) ? $phpmailer->ErrorInfo : 'Unknown error';
            $logger->Info("wp_mail failed. PHPMailer Error: " . $error);
        }
    }



    /**
     * Retrieve alarms from the database with optional filtering
     * 
     * @brief       Fetches stored alarms ordered by most recent first
     * @details     Retrieves alarms from the database table with optional filtering for
     *              unacknowledged alarms only. Results are always ordered by created_at DESC
     *              (most recent first). The limit is automatically constrained between 1 and 500
     *              to prevent excessive database queries.
     * 
     * @param int   $limit       Maximum number of alarms to retrieve (default: 50, min: 1, max: 500)
     * @param bool  $only_unacked If true, return only unacknowledged alarms (ack = 0); 
     *                            if false, return all alarms (default: false)
     * 
     * @return array|null Array of alarm records as associative arrays, or null on query failure.
     *                    Each alarm contains: id, created_at, severity, body, ack, ack_at, ack_by
     * 
     * @example
     *  $recent_alarms = CQtrAlarmsManager::get_alarms(10);  // Last 10 alarms
     *  $unacked = CQtrAlarmsManager::get_alarms(50, true);  // Last 50 unacknowledged alarms
     */
    public static function get_alarms($limit = 50, $only_unacked = false)
    {
        global $wpdb;
        $table = self::table_name();

        $limit = max(1, min(500, (int)$limit));

        if ($only_unacked) {
            return $wpdb->get_results(
                $wpdb->prepare(
                    "SELECT * FROM {$table} WHERE ack = 0 ORDER BY created_at DESC LIMIT %d",
                    $limit
                ),
                ARRAY_A
            );
        }

        return $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM {$table} ORDER BY created_at DESC LIMIT %d",
                $limit
            ),
            ARRAY_A
        );
    }


    /**
     * Mark an alarm as acknowledged by a user
     * 
     * @brief       Marks a specific alarm as acknowledged/read with timestamp and user tracking
     * @details     Updates an alarm record to indicate it has been reviewed/acknowledged by a user.
     *              Sets the ack flag to 1, records the acknowledgment timestamp, and tracks which
     *              user acknowledged it. Validates the alarm_id is positive before updating.
     *              Uses WordPress current time functions to ensure proper timezone handling.
     * 
     * @param int   $alarm_id The ID of the alarm to acknowledge. Must be > 0
     * @param int   $user_id  (optional) The WordPress user ID acknowledging the alarm.
     *                        If null, uses get_current_user_id() (default: null)
     * 
     * @return bool True if alarm was successfully updated, false if alarm_id invalid or update failed
     * 
     * @see acknowledge_all() to mark all unacknowledged alarms as read
     * 
     * @example
     *  if (CQtrAlarmsManager::acknowledge(42)) {
     *      echo "Alarm marked as read";
     *  } else {
     *      echo "Failed to acknowledge alarm";
     *  }
     */
    public static function acknowledge($alarm_id, $user_id = null)
    {
        global $wpdb;
        $table = self::table_name();

        $alarm_id = (int)$alarm_id;
        if ($alarm_id <= 0) return false;

        if ($user_id === null) {
            $user_id = get_current_user_id();
        }

        $now_gmt = current_time('mysql', true);
        $now_local = get_date_from_gmt($now_gmt, 'Y-m-d H:i:s');

        $updated = $wpdb->update(
            $table,
            array(
                'ack'    => 1,
                'ack_at' => $now_local,
                'ack_by' => (int)$user_id,
            ),
            array('id' => $alarm_id),
            array('%d','%s','%d'),
            array('%d')
        );

        return ($updated !== false);
    }


    /**
     * Mark all unacknowledged alarms as acknowledged by a user
     * 
     * @brief       Bulk acknowledges all unread alarms at once
     * @details     Updates all unacknowledged alarm records (ack = 0) to mark them as read.
     *              Sets the ack flag to 1 for all matching records, records a single acknowledgment
     *              timestamp, and tracks which user acknowledged them all. Useful for "Clear All"
     *              or "Mark as Read All" dashboard operations.
     *              Uses WordPress current time functions to ensure proper timezone handling.
     * 
     * @param int   $user_id (optional) The WordPress user ID acknowledging all alarms.
     *                       If null, uses get_current_user_id() (default: null)
     * 
     * @return bool True if update executed successfully (even if 0 alarms were updated),
     *             false if database query failed
     * 
     * @see acknowledge() to mark a single alarm as read
     * 
     * @example
     *  if (CQtrAlarmsManager::acknowledge_all()) {
     *      echo "All alarms marked as read";
     *  } else {
     *      echo "Failed to acknowledge alarms";
     *  }
     */
    public static function acknowledge_all($user_id = null)
    {
        global $wpdb;
        $table = self::table_name();

        if ($user_id === null) {
            $user_id = get_current_user_id();
        }

        $now_gmt = current_time('mysql', true);
        $now_local = get_date_from_gmt($now_gmt, 'Y-m-d H:i:s');

        // Only acknowledge currently unacked
        $sql = $wpdb->prepare(
            "UPDATE {$table}
             SET ack = 1, ack_at = %s, ack_by = %d
             WHERE ack = 0",
            $now_local,
            (int)$user_id
        );

        $updated = $wpdb->query($sql);
        return ($updated !== false);
    }

    /**
     * Get count of suppressed alarm emails today (by rate-limiting)
     * 
     * @return int Number of alarms whose emails were suppressed today
     */
    public static function get_suppressed_alarms_today()
    {
        $count = get_transient('qtr_suppressed_alarms_today');
        return $count ? (int)$count : 0;
    }

    /**
     * Reset suppressed alarm counter for today
     * 
     * @return bool True if reset successfully
     */
    public static function reset_suppressed_counter()
    {
        return delete_transient('qtr_suppressed_alarms_today');
    }

    /**
     * Delete old alarms, keeping only the most recent N alarms
     * 
     * Automatically called on alarm creation to prevent unbounded database growth.
     * Deletes oldest alarms first, preserving the N most recent alarms by created_at.
     * 
     * @param int $keep_count Number of most recent alarms to retain (default 100)
     * @return int|false Number of rows deleted or false on error
     */
    public static function cleanup_old_alarms($keep_count = 100)
    {
        global $wpdb;
        $table = self::table_name();
        $logger = new CQtrLogger();
        
        $keep_count = max(10, min(10000, (int)$keep_count));  // Safeguard: between 10 and 10000
        
        // Get count of alarms
        $total_count = (int)$wpdb->get_var("SELECT COUNT(*) FROM {$table}");
        
        if ($total_count <= $keep_count) {
            return 0;  // Nothing to delete
        }
        
        // Delete alarms not in the most recent N
        $sql = $wpdb->prepare(
            "DELETE FROM {$table}
             WHERE id NOT IN (
                SELECT id FROM (
                    SELECT id FROM {$table}
                    ORDER BY created_at DESC
                    LIMIT %d
                ) AS subquery
            )",
            $keep_count
        );
        
        $deleted = $wpdb->query($sql);
        
        if ($deleted > 0) {
            $logger->Info("Alarm cleanup: deleted {$deleted} old alarms (kept {$keep_count} most recent)");
        }
        
        return $deleted;
    }

    /**
     * Get all configured rate-limit settings
     * 
     * @return array Alarm type => cooldown minutes
     */
    public static function get_rate_limits()
    {
        return self::get_default_rate_limits();
    }

    /**
     * Clear all rate-limit transients (useful for testing/manual reset)
     * 
     * @return void
     */
    public static function clear_all_rate_limits()
    {
        global $wpdb;
        $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient%qtr_alarm_ratelimit_%'");
        $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name = '_transient_qtr_suppressed_alarms_today'");
    }
}