<?php
/**
 * Live Traffic Monitor — ring-buffer of last N requests for forensic review.
 *
 * Schreibt jeden Frontend-Request in einen kompakten Ring-Buffer (Default 200
 * Einträge). Admin-Page zeigt last-50 mit IP, UA, URL, Status, Zeit.
 * Filter: nur „interessante" Requests (Login-Attempts, 404, 403, Admin-Hits,
 * POST-Requests) — sonst würde es schnell unübersichtlich.
 *
 * Ring-Buffer-Strategie: Insert macht alle 100 Requests einen Cleanup auf
 * MAX_ROWS — verhindert unbounded growth ohne pro-Request-DELETE-Last.
 *
 * @package Brainwerk Security Suite
 */

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

class Shieldforge_Live_Traffic {

    const TABLE   = 'shieldforge_live_traffic';
    const MAX_ROWS = 1000;

    public static function register( Shieldforge_Loader $loader ) {
        if ( ! Shieldforge_Settings::get( 'live_traffic_enabled', false ) ) return;
        $loader->add_action( 'init', __CLASS__, 'maybe_record', 999 );
    }

    public static function maybe_install() {
        global $wpdb;
        $table = $wpdb->base_prefix . self::TABLE;
        $charset = $wpdb->get_charset_collate();
        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta( "CREATE TABLE {$table} (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            blog_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            created_at DATETIME NOT NULL,
            ip_anon VARCHAR(45) DEFAULT NULL,
            method VARCHAR(8) DEFAULT NULL,
            url_path VARCHAR(500) DEFAULT NULL,
            status_code SMALLINT UNSIGNED DEFAULT 200,
            user_agent VARCHAR(255) DEFAULT NULL,
            user_id BIGINT UNSIGNED DEFAULT 0,
            kind VARCHAR(20) DEFAULT NULL,
            PRIMARY KEY  (id),
            KEY idx_blog_created (blog_id, created_at),
            KEY idx_kind (kind)
        ) {$charset};" );
    }

    public static function maybe_record() {
        if ( is_admin() && ! wp_doing_ajax() ) return; // Admin-Hits ohne AJAX uninteressant
        $method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET';
        $uri    = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
        $is_login   = ( false !== strpos( $uri, 'wp-login.php' ) || false !== strpos( $uri, 'xmlrpc.php' ) );
        $is_admin   = ( false !== strpos( $uri, '/wp-admin' ) );
        $is_post    = ( 'POST' === $method );
        $is_ajax    = wp_doing_ajax();
        // Filter: NUR „interessante" Requests speichern (sonst unbounded)
        if ( ! $is_login && ! $is_admin && ! $is_post && ! $is_ajax ) return;

        global $wpdb;
        $table = $wpdb->base_prefix . self::TABLE;
        $ip = class_exists( 'Shieldforge_Utils' ) ? Shieldforge_Utils::get_real_ip() : ( isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '' );
        $ip_anon = self::anon_ip( $ip );
        $kind = $is_login ? 'login' : ( $is_admin ? 'admin' : ( $is_post ? 'post' : 'ajax' ) );
        $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? mb_substr( wp_strip_all_tags( sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) ), 0, 255 ) : '';
        $uid = get_current_user_id();
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- custom security table (not cacheable, no WP object-cache backing); direct live write by design.
        $wpdb->insert( $table, array(
            'blog_id'     => function_exists( 'get_current_blog_id' ) ? (int) get_current_blog_id() : 0,
            'created_at'  => current_time( 'mysql', 1 ),
            'ip_anon'     => $ip_anon,
            'method'      => $method,
            'url_path'    => mb_substr( $uri, 0, 500 ),
            'status_code' => http_response_code() ?: 200,
            'user_agent'  => $ua,
            'user_id'     => (int) $uid,
            'kind'        => $kind,
        ) );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery
        // Ring-Buffer-Cleanup ~ alle 100 Inserts
        if ( random_int( 1, 100 ) === 1 ) self::cleanup();
    }

    private static function anon_ip( $ip ) {
        if ( ! $ip ) return '';
        if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
            return preg_replace( '/\.\d+$/', '.0', $ip );
        }
        if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
            $parts = explode( ':', $ip );
            for ( $i = 4; $i < count( $parts ); $i++ ) $parts[ $i ] = '';
            return implode( ':', $parts );
        }
        return '';
    }

    public static function cleanup() {
        global $wpdb;
        $table = $wpdb->base_prefix . self::TABLE;
        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom security table (not cacheable, no WP object-cache backing); direct live read by design; user values use $wpdb->prepare().
        $cutoff_id = (int) $wpdb->get_var( $wpdb->prepare(
            "SELECT id FROM {$table} ORDER BY id DESC LIMIT 1 OFFSET %d", self::MAX_ROWS
        ) );
        if ( $cutoff_id > 0 ) {
            $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE id < %d", $cutoff_id ) );
        }
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    }

    public static function recent( $limit = 50 ) {
        global $wpdb;
        $table = $wpdb->base_prefix . self::TABLE;
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom security table (not cacheable, no WP object-cache backing); direct live read by design; user value uses $wpdb->prepare() placeholder.
        return (array) $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} ORDER BY id DESC LIMIT %d", (int) $limit ), ARRAY_A );
    }
}
