<?php
/**
 * Sensor Batcher — Pfad B (Threat-Intelligence Network).
 *
 * A bounded, local FIFO queue of SensorEvents waiting to be sent (RFC §3.1
 * Batcher.php). Events accumulate here until Transport::flush() peek()s /
 * remove()s a batch and ships it to the central API.
 *
 * Storage is a single non-autoloaded option (simple + dependency-free for the
 * expected volume). If volume warrants it, the backing store can be swapped
 * for a dedicated table behind this same interface — callers only use
 * enqueue()/peek()/remove()/count()/stats().
 *
 * Also keeps a small "what was queued for the network" audit ring so the admin
 * UI can show transparency (RFC §0: opt-in & transparency).
 *
 * @package Brainwerk Security Suite
 */

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

class Shieldforge_Sensor_Batcher {

    const QUEUE_OPTION = 'shieldforge_sensor_queue';
    const STATS_OPTION = 'shieldforge_sensor_stats';
    const AUDIT_OPTION = 'shieldforge_sensor_audit';

    const MAX_QUEUE = 1000; // hard cap; oldest dropped on overflow
    const MAX_AUDIT = 50;   // recent descriptors kept for the UI

    /* ------------------------------------------------------------------
     * Queue
     * ------------------------------------------------------------------ */

    /**
     * Append a SensorEvent to the queue. Enforces the cap by dropping the
     * oldest entries and counting the drops. Returns true on success.
     */
    public static function enqueue( array $sensor_event, array $meta = array() ) {
        $queue = self::read( self::QUEUE_OPTION );

        $id = isset( $sensor_event['event_id'] ) ? (string) $sensor_event['event_id'] : '';
        if ( '' === $id ) {
            return false;
        }
        // Idempotency: don't queue the same event_id twice.
        foreach ( $queue as $item ) {
            if ( isset( $item['id'] ) && $item['id'] === $id ) {
                return true;
            }
        }

        // share_with_feed is a TRANSPORT decision, kept on the queue wrapper —
        // never inside the SensorEvent, whose wire schema forbids the field.
        $queue[] = array(
            'id'              => $id,
            'event'           => $sensor_event,
            'share_with_feed' => ! empty( $meta['share_with_feed'] ),
            'queued_at'       => gmdate( 'Y-m-d\TH:i:s\Z' ),
            'attempts'        => 0,
        );

        $dropped = 0;
        if ( count( $queue ) > self::MAX_QUEUE ) {
            $dropped = count( $queue ) - self::MAX_QUEUE;
            $queue   = array_slice( $queue, $dropped );
        }

        self::write( self::QUEUE_OPTION, $queue );
        self::bump_stats( $dropped );
        return true;
    }

    /** Up to $max items from the head of the queue (oldest first), unmodified. */
    public static function peek( $max = 100 ) {
        $queue = self::read( self::QUEUE_OPTION );
        $max   = max( 1, (int) $max );
        return array_slice( $queue, 0, $max );
    }

    /** Remove queue items by their id (called after a successful send). */
    public static function remove( array $ids ) {
        if ( empty( $ids ) ) {
            return 0;
        }
        $drop  = array_fill_keys( array_map( 'strval', $ids ), true );
        $queue = self::read( self::QUEUE_OPTION );
        $before = count( $queue );
        $queue = array_values( array_filter(
            $queue,
            function ( $item ) use ( $drop ) {
                return ! isset( $drop[ $item['id'] ] );
            }
        ) );
        self::write( self::QUEUE_OPTION, $queue );
        return $before - count( $queue );
    }

    public static function count() {
        return count( self::read( self::QUEUE_OPTION ) );
    }

    public static function clear() {
        self::write( self::QUEUE_OPTION, array() );
    }

    /* ------------------------------------------------------------------
     * Stats + audit (transparency)
     * ------------------------------------------------------------------ */

    public static function stats() {
        $s = self::read( self::STATS_OPTION, false );
        if ( ! is_array( $s ) ) {
            $s = array();
        }
        return array(
            'queued_total'  => isset( $s['queued_total'] ) ? (int) $s['queued_total'] : 0,
            'dropped_total' => isset( $s['dropped_total'] ) ? (int) $s['dropped_total'] : 0,
            'last_enqueue'  => isset( $s['last_enqueue'] ) ? (string) $s['last_enqueue'] : '',
            'pending'       => self::count(),
        );
    }

    private static function bump_stats( $dropped ) {
        $s = self::read( self::STATS_OPTION, false );
        if ( ! is_array( $s ) ) {
            $s = array();
        }
        $s['queued_total']  = ( isset( $s['queued_total'] ) ? (int) $s['queued_total'] : 0 ) + 1;
        $s['dropped_total'] = ( isset( $s['dropped_total'] ) ? (int) $s['dropped_total'] : 0 ) + (int) $dropped;
        $s['last_enqueue']  = gmdate( 'Y-m-d\TH:i:s\Z' );
        self::write( self::STATS_OPTION, $s );
    }

    /** Append a transparency descriptor to the bounded audit ring. */
    public static function record_audit( array $descriptor ) {
        $audit   = self::read( self::AUDIT_OPTION );
        $audit[] = $descriptor;
        if ( count( $audit ) > self::MAX_AUDIT ) {
            $audit = array_slice( $audit, count( $audit ) - self::MAX_AUDIT );
        }
        self::write( self::AUDIT_OPTION, $audit );
    }

    /** Recent descriptors, newest first, for the admin "what gets sent" log. */
    public static function audit_log() {
        return array_reverse( self::read( self::AUDIT_OPTION ) );
    }

    /* ------------------------------------------------------------------
     * Option I/O (network-wide on multisite, site-local otherwise)
     * ------------------------------------------------------------------ */

    private static function read( $option, $default = array() ) {
        $v = is_multisite() ? get_site_option( $option, $default ) : get_option( $option, $default );
        return is_array( $v ) ? $v : ( false === $default ? false : array() );
    }

    private static function write( $option, $value ) {
        if ( is_multisite() ) {
            update_site_option( $option, $value );
        } else {
            update_option( $option, $value, false ); // never autoload the queue
        }
    }
}
