<?php
/**
 * Per-Visitor Message Limiter for BoostBot.
 *
 * Enforces hourly / daily message caps and burst-cooldown to prevent
 * excessive chatbot usage and deliberate token abuse.
 *
 * Storage: WordPress transients (same pattern as BoostBot_Rate_Limiter).
 *
 * @package BoostBot
 * @since   1.20.0
 */

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

class BoostBot_Message_Limiter {

    /* ──────────────────────────────────────────────────────────────── */
    /*  Burst cooldown thresholds (hardcoded, not admin-configurable)  */
    /* ──────────────────────────────────────────────────────────────── */

    /**
     * Burst detection window in seconds.
     */
    const BURST_WINDOW = 30;

    /**
     * Escalating cooldown tiers.
     * [ messages_in_window => cooldown_seconds ]
     * Evaluated from highest to lowest.
     */
    private static $burst_tiers = [
        12 => 10,
        8  => 5,
        5  => 3,
    ];

    /* ──────────────────────────────────────────────────────────────── */
    /*  Public API                                                      */
    /* ──────────────────────────────────────────────────────────────── */

    /**
     * Check whether a visitor is allowed to send a message right now.
     *
     * Does NOT increment counters — call increment() separately after
     * confirming the message will actually be processed.
     *
     * @param string $identifier  Visitor identifier (IP+UA hash).
     * @return array {
     *   allowed:          bool,
     *   limit_type:       string   ('hourly'|'daily'|'cooldown'|''),
     *   retry_after:      int      Seconds until the visitor can send again.
     *   cooldown_seconds: int      Active burst-cooldown (0 if none).
     *   remaining_hour:   int,
     *   remaining_day:    int,
     * }
     */
    public static function check( $identifier ) {
        $result = [
            'allowed'          => true,
            'limit_type'       => '',
            'retry_after'      => 0,
            'cooldown_seconds' => 0,
            'remaining_hour'   => 0,
            'remaining_day'    => 0,
        ];

        // Feature toggle
        if ( ! (bool) BoostBot_Settings::get( 'visitor_message_limit_enabled', true ) ) {
            $result['remaining_hour'] = PHP_INT_MAX;
            $result['remaining_day']  = PHP_INT_MAX;
            return $result;
        }

        $hour_limit = absint( BoostBot_Settings::get( 'visitor_messages_per_hour', 50 ) );
        $day_limit  = absint( BoostBot_Settings::get( 'visitor_messages_per_day', 200 ) );
        $hash       = self::hash( $identifier );

        // --- Daily check ---
        $day_data = self::get_counter( 'day', $hash, 86400 );
        $remaining_day = max( 0, $day_limit - $day_data['count'] );

        if ( $remaining_day <= 0 ) {
            $retry = max( 0, 86400 - ( time() - $day_data['start'] ) );
            return [
                'allowed'          => false,
                'limit_type'       => 'daily',
                'retry_after'      => $retry,
                'cooldown_seconds' => 0,
                'remaining_hour'   => 0,
                'remaining_day'    => 0,
            ];
        }

        // --- Hourly check ---
        $hour_data = self::get_counter( 'hour', $hash, 3600 );
        $remaining_hour = max( 0, $hour_limit - $hour_data['count'] );

        if ( $remaining_hour <= 0 ) {
            $retry = max( 0, 3600 - ( time() - $hour_data['start'] ) );
            return [
                'allowed'          => false,
                'limit_type'       => 'hourly',
                'retry_after'      => $retry,
                'cooldown_seconds' => 0,
                'remaining_hour'   => 0,
                'remaining_day'    => $remaining_day,
            ];
        }

        // --- Burst cooldown check ---
        $cooldown_enabled = (bool) BoostBot_Settings::get( 'visitor_cooldown_enabled', true );
        $cooldown_seconds = 0;

        if ( $cooldown_enabled ) {
            $cooldown_seconds = self::get_burst_cooldown( $hash );
        }

        if ( $cooldown_seconds > 0 ) {
            return [
                'allowed'          => false,
                'limit_type'       => 'cooldown',
                'retry_after'      => $cooldown_seconds,
                'cooldown_seconds' => $cooldown_seconds,
                'remaining_hour'   => $remaining_hour,
                'remaining_day'    => $remaining_day,
            ];
        }

        $result['remaining_hour'] = $remaining_hour;
        $result['remaining_day']  = $remaining_day;

        return $result;
    }

    /**
     * Increment all counters for this visitor (call after check() returns allowed=true).
     *
     * @param string $identifier Visitor identifier (IP+UA hash).
     * @return array { remaining_hour: int, remaining_day: int, cooldown_seconds: int }
     */
    public static function increment( $identifier ) {
        if ( ! (bool) BoostBot_Settings::get( 'visitor_message_limit_enabled', true ) ) {
            return [
                'remaining_hour'   => PHP_INT_MAX,
                'remaining_day'    => PHP_INT_MAX,
                'cooldown_seconds' => 0,
            ];
        }

        $hour_limit = absint( BoostBot_Settings::get( 'visitor_messages_per_hour', 50 ) );
        $day_limit  = absint( BoostBot_Settings::get( 'visitor_messages_per_day', 200 ) );
        $hash       = self::hash( $identifier );

        // Increment hourly counter
        $hour_data = self::increment_counter( 'hour', $hash, 3600 );

        // Increment daily counter
        $day_data = self::increment_counter( 'day', $hash, 86400 );

        // Increment burst tracker
        $cooldown_enabled = (bool) BoostBot_Settings::get( 'visitor_cooldown_enabled', true );
        $cooldown_seconds = 0;
        if ( $cooldown_enabled ) {
            self::record_burst_hit( $hash );
            $cooldown_seconds = self::get_burst_cooldown( $hash );
        }

        $remaining_hour = max( 0, $hour_limit - $hour_data['count'] );
        $remaining_day  = max( 0, $day_limit  - $day_data['count'] );

        return [
            'remaining_hour'   => $remaining_hour,
            'remaining_day'    => $remaining_day,
            'cooldown_seconds' => $cooldown_seconds,
        ];
    }

    /**
     * Get remaining message counts for a visitor (no side effects).
     *
     * @param string $identifier Visitor identifier.
     * @return array { remaining_hour: int, remaining_day: int }
     */
    public static function get_remaining( $identifier ) {
        if ( ! (bool) BoostBot_Settings::get( 'visitor_message_limit_enabled', true ) ) {
            return [ 'remaining_hour' => PHP_INT_MAX, 'remaining_day' => PHP_INT_MAX ];
        }

        $hour_limit = absint( BoostBot_Settings::get( 'visitor_messages_per_hour', 50 ) );
        $day_limit  = absint( BoostBot_Settings::get( 'visitor_messages_per_day', 200 ) );
        $hash       = self::hash( $identifier );

        $hour_data = self::get_counter( 'hour', $hash, 3600 );
        $day_data  = self::get_counter( 'day',  $hash, 86400 );

        return [
            'remaining_hour' => max( 0, $hour_limit - $hour_data['count'] ),
            'remaining_day'  => max( 0, $day_limit  - $day_data['count'] ),
        ];
    }

    /* ──────────────────────────────────────────────────────────────── */
    /*  Counter helpers (transient-based)                               */
    /* ──────────────────────────────────────────────────────────────── */

    /**
     * Build transient key.
     *
     * @param string $type 'hour' | 'day' | 'burst'
     * @param string $hash Short hash of identifier.
     * @return string
     */
    private static function transient_key( $type, $hash ) {
        return 'boostbot_msglim_' . $type . '_' . $hash;
    }

    /**
     * Shorten identifier to a 12-char hex hash.
     *
     * @param string $identifier
     * @return string
     */
    private static function hash( $identifier ) {
        return substr( md5( $identifier ), 0, 12 );
    }

    /**
     * Read a counter transient, auto-resetting if the window has expired.
     *
     * @param string $type   'hour' | 'day'
     * @param string $hash   Visitor hash.
     * @param int    $window Window duration in seconds.
     * @return array { count: int, start: int }
     */
    private static function get_counter( $type, $hash, $window ) {
        $key  = self::transient_key( $type, $hash );
        $data = get_transient( $key );

        if ( false === $data ) {
            return [ 'count' => 0, 'start' => time() ];
        }

        // Window expired — treat as fresh.
        if ( time() - $data['start'] >= $window ) {
            delete_transient( $key );
            return [ 'count' => 0, 'start' => time() ];
        }

        return $data;
    }

    /**
     * Increment a counter transient.
     *
     * @param string $type   'hour' | 'day'
     * @param string $hash   Visitor hash.
     * @param int    $window Window duration in seconds.
     * @return array { count: int, start: int } After increment.
     */
    private static function increment_counter( $type, $hash, $window ) {
        $key  = self::transient_key( $type, $hash );
        $data = get_transient( $key );

        if ( false === $data || ( time() - $data['start'] >= $window ) ) {
            // First message in this window.
            $data = [
                'count' => 1,
                'start' => time(),
            ];
            set_transient( $key, $data, $window );
            return $data;
        }

        $data['count']++;
        set_transient( $key, $data, $window );

        return $data;
    }

    /* ──────────────────────────────────────────────────────────────── */
    /*  Burst detection                                                 */
    /* ──────────────────────────────────────────────────────────────── */

    /**
     * Record a message timestamp for burst detection.
     *
     * @param string $hash Visitor hash.
     */
    private static function record_burst_hit( $hash ) {
        $key  = self::transient_key( 'burst', $hash );
        $hits = get_transient( $key );

        if ( ! is_array( $hits ) ) {
            $hits = [];
        }

        $now       = time();
        $threshold = $now - self::BURST_WINDOW;

        // Prune old hits outside the window.
        $hits = array_values( array_filter( $hits, function ( $ts ) use ( $threshold ) {
            return $ts >= $threshold;
        } ) );

        $hits[] = $now;

        set_transient( $key, $hits, self::BURST_WINDOW + 5 );
    }

    /**
     * Calculate the current burst cooldown for a visitor.
     *
     * @param string $hash Visitor hash.
     * @return int Cooldown seconds (0 if none).
     */
    private static function get_burst_cooldown( $hash ) {
        $key  = self::transient_key( 'burst', $hash );
        $hits = get_transient( $key );

        if ( ! is_array( $hits ) || empty( $hits ) ) {
            return 0;
        }

        $now       = time();
        $threshold = $now - self::BURST_WINDOW;

        // Count recent hits within window.
        $recent = array_filter( $hits, function ( $ts ) use ( $threshold ) {
            return $ts >= $threshold;
        } );
        $count = count( $recent );

        // Match against tiers (highest first).
        foreach ( self::$burst_tiers as $min_msgs => $cooldown ) {
            if ( $count >= $min_msgs ) {
                // Cooldown is relative to the last hit.
                $last_hit = max( $recent );
                $elapsed  = $now - $last_hit;
                $remaining = max( 0, $cooldown - $elapsed );
                return $remaining;
            }
        }

        return 0;
    }
}
