<?php
namespace EasyChat\Library\Messenger;

use EasyChat\Library\Security\AeadEnvelopeCipher;
use EasyChat\Library\Security\PrivateFileArea;








final class MaxOperationalSpool {
    private const FORMAT = 1;
    private const MAX_FILES = 1000;
    private const MAX_BYTES = 8192;
    private const CATEGORIES = ['authorization', 'database', 'storage'];
    private $files;
    private $cipher;

    public function __construct(?PrivateFileArea $files = null, ?AeadEnvelopeCipher $cipher = null) {
        $this->files = $files ?: MaxPrivateRuntime::fileArea(MaxPrivateRuntime::root() . DIRECTORY_SEPARATOR . 'operational');
        $this->cipher = $cipher ?: MaxPrivateRuntime::cipher();
    }

    public function append(int $channel_id, int $generation, string $category, ?int $occurred_at = null): bool {
        if ($channel_id <= 0 || $generation < 0 || !in_array($category, self::CATEGORIES, true)) {
            return false;
        }
        $lock = $this->lock();
        try {
            $paths = $this->paths();
            if (count($paths) >= self::MAX_FILES) {
                return false;
            }
            $event = [
                'format' => self::FORMAT,
                'channel_id' => $channel_id,
                'generation' => $generation,
                'category' => $category,
                'occurred_at' => max(1, $occurred_at ?: time()),
            ];
            $json = json_encode($event, JSON_UNESCAPED_SLASHES);
            if (!is_string($json)) {
                return false;
            }
            $name = 'incident-t' . sprintf('%013d', (int)floor(microtime(true) * 1000))
                . '-' . bin2hex(random_bytes(12)) . '.enc';
            $this->files->writeAtomic($this->files->path($name), $this->cipher->encrypt($this->context(), $json));
            return true;
        } finally {
            flock($lock, LOCK_UN);
            fclose($lock);
        }
    }

     
    public function drain(callable $consumer, int $limit = 50): array {
        $lock = $this->lock();
        $processed = 0;
        $failed = 0;
        try {
            foreach (array_slice($this->paths(), 0, max(1, min(200, $limit))) as $path) {
                try {
                    $event = $this->read($path);
                    if (call_user_func($consumer, $event) !== true) {
                        throw new \RuntimeException('max_operational_spool_consumer_failed');
                    }
                    $this->files->delete($path);
                    $processed++;
                } catch (\Throwable $e) {
                     
                     
                    $failed++;
                    break;
                }
            }
            return ['processed' => $processed, 'failed' => $failed, 'remaining' => count($this->paths())];
        } finally {
            flock($lock, LOCK_UN);
            fclose($lock);
        }
    }

    private function read(string $path): array {
        $envelope = $this->files->read($path, self::MAX_BYTES);
        $json = is_string($envelope) ? $this->cipher->decrypt($this->context(), $envelope) : '';
        $event = json_decode($json, true);
        if (!is_array($event)
            || array_keys($event) !== ['format', 'channel_id', 'generation', 'category', 'occurred_at']
            || (int)$event['format'] !== self::FORMAT
            || !is_int($event['channel_id']) || $event['channel_id'] <= 0
            || !is_int($event['generation']) || $event['generation'] < 0
            || !is_string($event['category']) || !in_array($event['category'], self::CATEGORIES, true)
            || !is_int($event['occurred_at']) || $event['occurred_at'] <= 0) {
            throw new \RuntimeException('max_operational_spool_event_invalid');
        }
        return $event;
    }

    private function paths(): array {
        $directory = dirname($this->files->path('events.probe'));
        $paths = glob($directory . DIRECTORY_SEPARATOR . 'incident-t*-*.enc') ?: [];
        sort($paths, SORT_STRING);
        return $paths;
    }

    private function lock() {
        $lock = $this->files->openLock($this->files->path('operational.lock'));
        if (!is_resource($lock)) {
            throw new \RuntimeException('max_operational_spool_lock_failed');
        }
        return $lock;
    }

    private function context(): string {
        return "max-operational-spool\0v1";
    }
}
