<?php

declare(strict_types=1);

namespace SecurWP\Modules\Firewall\Blocklist;

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

/**
 * Transient-backed buffer for crowd-sourced threat reports.
 *
 * Queues attack reports (IP + type) until the next blocklist sync, at which
 * point CloudIntelProvider peeks the buffer and piggybacks the reports on the
 * blocklist POST request. The buffer is only cleared after a successful sync,
 * so transient cloud failures do not drop queued threat intel.
 *
 * Buffer is stored as a WordPress transient with a 1-day TTL and capped
 * at MAX_ENTRIES to prevent memory issues. Entries are deduplicated by
 * ip+type.
 *
 */
class ReportBuffer {

    /**
     * Transient key for the report buffer.
     */
    private const TRANSIENT_KEY = 'securwp_network_report_buffer';

    /**
     * TTL in seconds (1 day). Reports expire naturally if no successful
     * cloud sync clears them within this window.
     */
    private const TTL = 86400;

    /**
     * Maximum entries in the buffer. Oldest are dropped when exceeded.
     */
    private const MAX_ENTRIES = 200;

    /**
     * Valid report types that the cloud endpoint accepts.
     */
    private const VALID_TYPES = [
        'failed_login',
        'sensitive_path',
        '404_flood',
        'xmlrpc',
    ];

    /**
     * Add a report to the buffer.
     *
     * @param string $ip   The IP address to report.
     * @param string $type The attack type (failed_login, sensitive_path, 404_flood, xmlrpc).
     */
    public static function add( string $ip, string $type ): void {
        $ip   = trim( $ip );
        $type = trim( $type );

        if ( $ip === '' || ! in_array( $type, self::VALID_TYPES, true ) ) {
            return;
        }

        // Skip private/loopback IPs.
        if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
            return;
        }

        $buffer = self::get_buffer();

        // Deduplicate by ip+type.
        $key = $ip . '|' . $type;
        if ( isset( $buffer[ $key ] ) ) {
            return;
        }

        $buffer[ $key ] = [
            'ip'   => $ip,
            'type' => $type,
        ];

        // Cap at MAX_ENTRIES (drop oldest when exceeded).
        if ( count( $buffer ) > self::MAX_ENTRIES ) {
            $buffer = array_slice( $buffer, -self::MAX_ENTRIES, null, true );
        }

        set_transient( self::TRANSIENT_KEY, $buffer, self::TTL );
    }

    /**
     * Read the current queued reports without clearing them.
     *
     * @return array[] Array of ['ip' => string, 'type' => string] entries.
     */
    public static function peek(): array {
        $buffer = self::get_buffer();

        if ( empty( $buffer ) ) {
            return [];
        }

        return array_values( $buffer );
    }

    /**
     * Clear the queued reports after a successful sync.
     */
    public static function clear(): void {
        delete_transient( self::TRANSIENT_KEY );
    }

    /**
     * Get the current buffer contents.
     *
     * @return array Associative array keyed by "ip|type".
     */
    private static function get_buffer(): array {
        $buffer = get_transient( self::TRANSIENT_KEY );
        return is_array( $buffer ) ? $buffer : [];
    }
}
