<?php

/**
 * Sentinel Incidents Class
 *
 * Handles security incident detection, evaluation, creation and management.
 * Evaluates counter snapshots against detection rules and manages incident lifecycle.
 */

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

class Sentinel_Incidents
{

    /**
     * Incident types
     */
    const TYPE_BRUTEFORCE = 'security.bruteforce';
    const TYPE_ENUMERATION = 'security.enumeration';
    const TYPE_XMLRPC_FLOOD = 'security.xmlrpc_flood';

    /**
     * Confidence levels
     */
    const CONFIDENCE_LOW = 'low';
    const CONFIDENCE_MEDIUM = 'medium';
    const CONFIDENCE_HIGH = 'high';

    /**
     * Incident status
     */
    const STATUS_OPEN = 'open';
    const STATUS_RESOLVED = 'resolved';

    /**
     * Evaluate counter snapshot against detection rules and create/update incidents
     *
     * @param array $snapshot Counter snapshot from Sentinel_Auth_Counters
     * @param array $ctx Authentication context (ip, username, source, etc.)
     * @return array|null Incident data if threshold met, null otherwise
     */
    public static function evaluate_and_handle($snapshot, $ctx)
    {
        if (empty($snapshot) || empty($ctx['ip'])) {
            return null;
        }

        $security_settings = get_option('sentinel_security_settings', array());
        $ip = $ctx['ip'];
        $current_time = current_time('mysql');

        // Check each detection rule
        $incidents_triggered = array();

        // 1. Brute Force Detection (per IP)
        $bf_threshold = $security_settings['bruteforce_threshold'] ?? 5;
        if ($snapshot['fails'] >= $bf_threshold) {
            $incident_data = array(
                'type' => self::TYPE_BRUTEFORCE,
                'ip' => $ip,
                'confidence' => self::CONFIDENCE_LOW,
                'meta' => array(
                    'fails_count' => $snapshot['fails'],
                    'threshold' => $bf_threshold,
                    'source' => $snapshot['source'] ?? 'mixed',
                    'rule_triggered' => 'bruteforce_threshold',
                    'detection_time' => $current_time,
                    'usernames_attempted' => $snapshot['distinct_usernames'] ?? 0
                )
            );
            $incidents_triggered[] = $incident_data;
        }

        // 2. Username Enumeration Detection (per IP)
        $enum_threshold = $security_settings['enumeration_threshold'] ?? 10;
        if ($snapshot['distinct_usernames'] >= $enum_threshold) {
            $incident_data = array(
                'type' => self::TYPE_ENUMERATION,
                'ip' => $ip,
                'confidence' => self::CONFIDENCE_LOW,
                'meta' => array(
                    'usernames_count' => $snapshot['distinct_usernames'],
                    'threshold' => $enum_threshold,
                    'fails_count' => $snapshot['fails'],
                    'rule_triggered' => 'enumeration_threshold',
                    'detection_time' => $current_time,
                    'source' => $snapshot['source'] ?? 'mixed'
                )
            );
            $incidents_triggered[] = $incident_data;
        }

        // 3. XML-RPC Flood Detection
        $xmlrpc_threshold = $security_settings['xmlrpc_threshold'] ?? 20;
        if ($snapshot['xmlrpc_count'] >= $xmlrpc_threshold) {
            $incident_data = array(
                'type' => self::TYPE_XMLRPC_FLOOD,
                'ip' => $ip,
                'confidence' => self::CONFIDENCE_LOW,
                'meta' => array(
                    'xmlrpc_count' => $snapshot['xmlrpc_count'],
                    'threshold' => $xmlrpc_threshold,
                    'fails_count' => $snapshot['fails'],
                    'rule_triggered' => 'xmlrpc_flood_threshold',
                    'detection_time' => $current_time,
                    'source' => 'xmlrpc'
                )
            );
            $incidents_triggered[] = $incident_data;
        }

        // Handle each triggered incident
        foreach ($incidents_triggered as $incident_data) {
            self::handle_incident($incident_data, $ctx);
        }

        return !empty($incidents_triggered) ? $incidents_triggered[0] : null;
    }

    /**
     * Handle a detected incident (create new or update existing)
     *
     * @param array $incident_data Incident data from evaluation
     * @param array $ctx Authentication context
     */
    private static function handle_incident($incident_data, $ctx)
    {
        // Check if there's already an open incident of this type for this IP
        $existing_incident = self::find_open_incident($incident_data['ip'], $incident_data['type']);

        if ($existing_incident) {
            // Update existing incident
            self::update_incident($existing_incident['id'], $incident_data, $ctx);
        } else {
            // Create new incident
            self::create_incident($incident_data, $ctx);
        }
    }

    /**
     * Create a new security incident
     *
     * @param array $incident_data Incident data
     * @param array $ctx Authentication context
     * @return int|false Incident ID on success, false on failure
     */
    public static function create_incident($incident_data, $ctx)
    {
        global $wpdb;

        $current_time = current_time('mysql');
        $table_name = $wpdb->prefix . 'sentinel_incidents';

        // Pack IP address for storage
        $packed_ip = inet_pton($incident_data['ip']);
        if ($packed_ip === false) {
            // Log the invalid IP error for debugging
            if (function_exists('error_log')) {
                error_log('[Sentinel] Invalid IP address for incident: ' . $incident_data['ip']);
            }
            return false; // Invalid IP address
        }

        // Prepare metadata
        $meta = $incident_data['meta'] ?? array();
        $meta['created_by_rule'] = $meta['rule_triggered'] ?? 'unknown';
        $meta['initial_context'] = array(
            'username' => $ctx['username'] ?? null,
            'user_agent' => $ctx['ua'] ?? null,
            'source' => $ctx['source'] ?? null,
            'url' => $ctx['url'] ?? null
        );

        // Insert incident record
        $result = $wpdb->insert(
            $table_name,
            array(
                'type' => $incident_data['type'],
                'status' => self::STATUS_OPEN,
                'confidence' => $incident_data['confidence'],
                'ip' => $packed_ip,
                'username' => $ctx['username'] ?? null,
                'source' => $ctx['source'] ?? null,
                'first_seen' => $current_time,
                'last_seen' => $current_time,
                'count' => 1,
                'meta' => wp_json_encode($meta),
                'created_at' => $current_time,
                'updated_at' => $current_time
            ),
            array(
                '%s', // type
                '%s', // status
                '%s', // confidence
                '%s', // ip (binary)
                '%s', // username
                '%s', // source
                '%s', // first_seen
                '%s', // last_seen
                '%d', // count
                '%s', // meta (json)
                '%s', // created_at
                '%s'  // updated_at
            )
        );

        if ($result === false) {
            return false;
        }

        $incident_id = $wpdb->insert_id;

        // Log incident creation event
        if (function_exists('sentinel_log_event')) {
            sentinel_log_event('security_incident_opened', array(
                'incident_id' => $incident_id,
                'incident_type' => $incident_data['type'],
                'ip_address' => $incident_data['ip'],
                'confidence' => $incident_data['confidence'],
                'rule_triggered' => $meta['rule_triggered'] ?? 'unknown',
                'threshold_data' => $meta
            ));
        }

        // Send notification about new incident
        self::notify_incident_event($incident_id, 'opened', $incident_data);

        return $incident_id;
    }

    /**
     * Update an existing incident
     *
     * @param int $incident_id Incident ID
     * @param array $incident_data New incident data
     * @param array $ctx Authentication context
     * @return bool True on success
     */
    public static function update_incident($incident_id, $incident_data, $ctx)
    {
        global $wpdb;

        $table_name = $wpdb->prefix . 'sentinel_incidents';
        $current_time = current_time('mysql');

        // Get current incident data
        $current_incident = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM $table_name WHERE id = %d",
            $incident_id
        ), ARRAY_A);

        if (!$current_incident) {
            return false;
        }

        // Determine if confidence should escalate
        $new_confidence = self::calculate_confidence_escalation(
            $current_incident['confidence'],
            $incident_data,
            $ctx
        );

        // Update count and metadata
        $current_meta = json_decode($current_incident['meta'], true) ?: array();
        $new_count = intval($current_incident['count']) + 1;

        // Add new event to sample history
        if (!isset($current_meta['event_samples'])) {
            $current_meta['event_samples'] = array();
        }

        // Keep only last 10 samples to prevent bloat
        if (count($current_meta['event_samples']) >= 10) {
            $current_meta['event_samples'] = array_slice($current_meta['event_samples'], -9);
        }

        $current_meta['event_samples'][] = array(
            'timestamp' => $current_time,
            'username' => $ctx['username'] ?? null,
            'source' => $ctx['source'] ?? null,
            'user_agent' => $ctx['ua'] ?? null
        );

        // Update latest threshold data
        $current_meta = array_merge($current_meta, $incident_data['meta'] ?? array());
        $current_meta['last_updated'] = $current_time;

        // Perform database update
        $result = $wpdb->update(
            $table_name,
            array(
                'confidence' => $new_confidence,
                'last_seen' => $current_time,
                'count' => $new_count,
                'meta' => wp_json_encode($current_meta),
                'updated_at' => $current_time
            ),
            array('id' => $incident_id),
            array('%s', '%s', '%d', '%s', '%s'),
            array('%d')
        );

        if ($result === false) {
            return false;
        }

        // Check if we should notify (only if confidence escalated or significant time passed)
        $should_notify = (
            $new_confidence !== $current_incident['confidence'] ||
            self::should_notify_on_update($current_incident)
        );

        if ($should_notify) {
            // Log incident update event
            if (function_exists('sentinel_log_event')) {
                sentinel_log_event('security_incident_updated', array(
                    'incident_id' => $incident_id,
                    'incident_type' => $current_incident['type'],
                    'ip_address' => inet_ntop($current_incident['ip']),
                    'old_confidence' => $current_incident['confidence'],
                    'new_confidence' => $new_confidence,
                    'total_count' => $new_count,
                    'update_reason' => $new_confidence !== $current_incident['confidence'] ? 'confidence_escalation' : 'periodic_update'
                ));
            }

            // Send notification
            $updated_incident_data = array_merge($incident_data, array(
                'id' => $incident_id,
                'confidence' => $new_confidence,
                'count' => $new_count
            ));
            self::notify_incident_event($incident_id, 'updated', $updated_incident_data);
        }

        return true;
    }

    /**
     * Handle success-after-burst confidence escalation
     *
     * @param string $ip IP address
     * @param string $username Username that successfully logged in
     * @return bool True if any incident was escalated
     */
    public static function escalate_on_success_after_burst($ip, $username)
    {
        global $wpdb;

        $table_name = $wpdb->prefix . 'sentinel_incidents';
        $packed_ip = inet_pton($ip);

        if ($packed_ip === false) {
            return false;
        }

        // Find open brute force or enumeration incidents for this IP
        $fifteen_min_ago = date('Y-m-d H:i:s', strtotime('-15 minutes'));
        $sql = "SELECT * FROM $table_name
                WHERE ip = %s
                AND status = %s
                AND (type = %s OR type = %s)
                AND last_seen >= %s";

        $open_incidents = $wpdb->get_results($wpdb->prepare(
            $sql,
            $packed_ip,
            self::STATUS_OPEN,
            self::TYPE_BRUTEFORCE,
            self::TYPE_ENUMERATION,
            $fifteen_min_ago
        ), ARRAY_A);

        $escalated_count = 0;

        foreach ($open_incidents as $incident) {
            // Only escalate if not already at high confidence
            if ($incident['confidence'] !== self::CONFIDENCE_HIGH) {
                $meta = json_decode($incident['meta'], true) ?: array();
                $meta['success_after_burst'] = array(
                    'username' => $username,
                    'timestamp' => current_time('mysql'),
                    'escalation_reason' => 'successful_login_after_failed_attempts'
                );

                $result = $wpdb->update(
                    $table_name,
                    array(
                        'confidence' => self::CONFIDENCE_HIGH,
                        'meta' => wp_json_encode($meta),
                        'updated_at' => current_time('mysql')
                    ),
                    array('id' => $incident['id']),
                    array('%s', '%s', '%s'),
                    array('%d')
                );

                if ($result !== false) {
                    $escalated_count++;

                    // Log escalation
                    if (function_exists('sentinel_log_event')) {
                        sentinel_log_event('security_incident_updated', array(
                            'incident_id' => $incident['id'],
                            'incident_type' => $incident['type'],
                            'ip_address' => $ip,
                            'old_confidence' => $incident['confidence'],
                            'new_confidence' => self::CONFIDENCE_HIGH,
                            'escalation_reason' => 'success_after_burst',
                            'successful_username' => $username
                        ));
                    }

                    // Send high-priority notification
                    $incident_data = array(
                        'id' => $incident['id'],
                        'type' => $incident['type'],
                        'ip' => $ip,
                        'confidence' => self::CONFIDENCE_HIGH,
                        'username' => $username,
                        'escalation_reason' => 'success_after_burst'
                    );
                    self::notify_incident_event($incident['id'], 'escalated', $incident_data);
                }
            }
        }

        return $escalated_count > 0;
    }

    /**
     * Auto-resolve stale incidents
     * Called by cron job to clean up incidents with no recent activity
     *
     * @return int Number of incidents resolved
     */
    public static function auto_resolve_stale_incidents()
    {
        global $wpdb;

        $security_settings = get_option('sentinel_security_settings', array());
        $auto_resolve_hours = $security_settings['incident_auto_resolve'] ?? 21600; // Default 6 hours

        $table_name = $wpdb->prefix . 'sentinel_incidents';
        $current_time = current_time('mysql');

        // Find incidents that haven't been updated recently
        $stale_incidents = $wpdb->get_results($wpdb->prepare(
            "SELECT id, type, ip, meta FROM $table_name
             WHERE status = %s
             AND last_seen < DATE_SUB(%s, INTERVAL %d SECOND)",
            self::STATUS_OPEN,
            $current_time,
            $auto_resolve_hours
        ), ARRAY_A);

        $resolved_count = 0;

        foreach ($stale_incidents as $incident) {
            $result = $wpdb->update(
                $table_name,
                array(
                    'status' => self::STATUS_RESOLVED,
                    'updated_at' => $current_time
                ),
                array('id' => $incident['id']),
                array('%s', '%s'),
                array('%d')
            );

            if ($result !== false) {
                $resolved_count++;

                // Log resolution
                if (function_exists('sentinel_log_event')) {
                    sentinel_log_event('security_incident_resolved', array(
                        'incident_id' => $incident['id'],
                        'incident_type' => $incident['type'],
                        'ip_address' => inet_ntop($incident['ip']),
                        'resolution_reason' => 'auto_resolved_stale',
                        'stale_duration_hours' => round($auto_resolve_hours / 3600, 1)
                    ));
                }
            }
        }

        return $resolved_count;
    }

    /**
     * Find open incident by IP and type
     *
     * @param string $ip IP address
     * @param string $type Incident type
     * @return array|null Incident data or null if not found
     */
    public static function find_open_incident($ip, $type)
    {
        global $wpdb;

        $table_name = $wpdb->prefix . 'sentinel_incidents';
        $packed_ip = inet_pton($ip);

        if ($packed_ip === false) {
            return null;
        }

        $incident = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM $table_name
             WHERE ip = %s
             AND type = %s
             AND status = %s
             ORDER BY created_at DESC
             LIMIT 1",
            $packed_ip,
            $type,
            self::STATUS_OPEN
        ), ARRAY_A);

        if ($incident && $incident['ip']) {
            // Convert binary IP back to string
            $incident['ip_string'] = inet_ntop($incident['ip']);
            $incident['meta_decoded'] = json_decode($incident['meta'], true) ?: array();
        }

        return $incident;
    }

    /**
     * Calculate confidence escalation based on incident history
     *
     * @param string $current_confidence Current confidence level
     * @param array $incident_data New incident data
     * @param array $ctx Context data
     * @return string New confidence level
     */
    private static function calculate_confidence_escalation($current_confidence, $incident_data, $ctx)
    {
        // Rule 1: Repeated threshold breaches escalate to medium
        if ($current_confidence === self::CONFIDENCE_LOW) {
            return self::CONFIDENCE_MEDIUM;
        }

        // Rule 2: Multiple different rule types from same IP (handled elsewhere)
        // Rule 3: Success after burst (handled in separate function)

        // Default: no change
        return $current_confidence;
    }

    /**
     * Check if incident should trigger notification on update
     *
     * @param array $incident Current incident data
     * @return bool True if should notify
     */
    private static function should_notify_on_update($incident)
    {
        $security_settings = get_option('sentinel_security_settings', array());
        $cooldown_seconds = $security_settings['notification_cooldown'] ?? 1800; // 30 minutes

        $last_updated = strtotime($incident['updated_at']);
        $cooldown_expired = (time() - $last_updated) >= $cooldown_seconds;

        return $cooldown_expired;
    }

    /**
     * Send notification about incident event
     *
     * @param int $incident_id Incident ID
     * @param string $event Event type (opened|updated|escalated|resolved)
     * @param array $incident_data Incident data
     */
    private static function notify_incident_event($incident_id, $event, $incident_data)
    {
        // Integration point for notification system
        // This will be connected to the existing Sentinel notification system

        do_action('sentinel_security_incident_notification', array(
            'incident_id' => $incident_id,
            'event' => $event,
            'type' => $incident_data['type'],
            'ip' => $incident_data['ip'],
            'confidence' => $incident_data['confidence'],
            'data' => $incident_data
        ));
    }

    /**
     * Get incident statistics for dashboard
     *
     * @param int $days Number of days to look back (default 7)
     * @return array Statistics array
     */
    public static function get_incident_stats($days = 7)
    {
        global $wpdb;

        $table_name = $wpdb->prefix . 'sentinel_incidents';

        $stats = array();

        // Open incidents count
        $stats['open_incidents'] = $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM $table_name WHERE status = %s",
            self::STATUS_OPEN
        ));

        // Incidents in last X days
        $stats['recent_incidents'] = $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM $table_name
             WHERE created_at >= DATE_SUB(NOW(), INTERVAL %d DAY)",
            $days
        ));

        // Top offender IPs (last 7 days)
        $top_ips = $wpdb->get_results($wpdb->prepare(
            "SELECT ip, COUNT(*) as incident_count
             FROM $table_name
             WHERE created_at >= DATE_SUB(NOW(), INTERVAL %d DAY)
             GROUP BY ip
             ORDER BY incident_count DESC
             LIMIT 5",
            $days
        ), ARRAY_A);

        $stats['top_offender_ips'] = array();
        foreach ($top_ips as $row) {
            $stats['top_offender_ips'][] = array(
                'ip' => inet_ntop($row['ip']),
                'incidents' => intval($row['incident_count'])
            );
        }

        // Incident types breakdown
        $types = $wpdb->get_results($wpdb->prepare(
            "SELECT type, COUNT(*) as count
             FROM $table_name
             WHERE created_at >= DATE_SUB(NOW(), INTERVAL %d DAY)
             GROUP BY type",
            $days
        ), ARRAY_A);

        $stats['incident_types'] = array();
        foreach ($types as $row) {
            $stats['incident_types'][$row['type']] = intval($row['count']);
        }

        return $stats;
    }

    /**
     * Get incidents list for admin UI
     *
     * @param array $filters Filter options
     * @param int $limit Number of incidents to return
     * @param int $offset Offset for pagination
     * @return array Incidents with metadata
     */
    public static function get_incidents_list($filters = array(), $limit = 50, $offset = 0)
    {
        global $wpdb;

        $table_name = $wpdb->prefix . 'sentinel_incidents';

        $where_clauses = array();
        $where_values = array();

        // Apply filters
        if (!empty($filters['status'])) {
            $where_clauses[] = "status = %s";
            $where_values[] = $filters['status'];
        }

        if (!empty($filters['type'])) {
            $where_clauses[] = "type = %s";
            $where_values[] = $filters['type'];
        }

        if (!empty($filters['confidence'])) {
            $where_clauses[] = "confidence = %s";
            $where_values[] = $filters['confidence'];
        }

        if (!empty($filters['ip'])) {
            $packed_ip = inet_pton($filters['ip']);
            if ($packed_ip !== false) {
                $where_clauses[] = "ip = %s";
                $where_values[] = $packed_ip;
            }
        }

        $where_sql = !empty($where_clauses) ? 'WHERE ' . implode(' AND ', $where_clauses) : '';

        $incidents = $wpdb->get_results($wpdb->prepare(
            "SELECT * FROM $table_name
             $where_sql
             ORDER BY created_at DESC
             LIMIT %d OFFSET %d",
            array_merge($where_values, array($limit, $offset))
        ), ARRAY_A);

        // Process incidents for display
        foreach ($incidents as &$incident) {
            $incident['ip_string'] = inet_ntop($incident['ip']);
            $incident['meta_decoded'] = json_decode($incident['meta'], true) ?: array();
            $incident['created_at_formatted'] = date('Y-m-d H:i:s', strtotime($incident['created_at']));
            $incident['last_seen_formatted'] = date('Y-m-d H:i:s', strtotime($incident['last_seen']));
        }

        return $incidents;
    }

    /**
     * Get incident by ID
     *
     * @param int $incident_id Incident ID
     * @return array|null Incident data or null if not found
     */
    public static function get_incident_by_id($incident_id)
    {
        global $wpdb;

        $table_name = $wpdb->prefix . 'sentinel_incidents';

        $incident = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM $table_name WHERE id = %d",
            $incident_id
        ), ARRAY_A);

        return $incident ?: null;
    }

    /**
     * Resolve an incident
     *
     * @param int $incident_id Incident ID
     * @param string $reason Resolution reason (manual|auto)
     * @return bool Success status
     */
    public static function resolve_incident($incident_id, $reason = 'manual')
    {
        global $wpdb;

        $table_name = $wpdb->prefix . 'sentinel_incidents';

        $result = $wpdb->update(
            $table_name,
            array(
                'status' => self::STATUS_RESOLVED,
                'updated_at' => current_time('mysql')
            ),
            array('id' => $incident_id),
            array('%s', '%s'),
            array('%d')
        );

        if ($result !== false) {
            // Log resolution event
            if (function_exists('sentinel_log_event')) {
                sentinel_log_event('security_incident_resolved', array(
                    'incident_id' => $incident_id,
                    'resolution_reason' => $reason,
                    'resolved_by' => get_current_user_id()
                ));
            }

            // Send notification
            $incident = self::get_incident_by_id($incident_id);
            if ($incident) {
                $incident_data = array(
                    'id' => $incident_id,
                    'type' => $incident['type'],
                    'ip' => inet_ntop($incident['ip']),
                    'resolution_reason' => $reason
                );
                self::notify_incident_event($incident_id, 'resolved', $incident_data);
            }

            return true;
        }

        return false;
    }
}
