<?php
namespace EasyChat\Library\Messenger;

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







final class MaxDurableInbox {
    private const FORMAT = 1;
    private const MAX_EVENT_BYTES = 1048576;
    private const MAX_TOTAL_BYTES = 104857600;
    private const MAX_EVENTS = 10000;
    private $files;
    private $cipher;

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

    public function acceptAndProcess(int $channel_id, int $generation, array $event, callable $processor): array {
        $identity = $this->identity($event);
        $path = $this->eventPath($channel_id, $generation, $identity);
        $lock = $this->channelLock($channel_id);
        try {
            if (!is_file($path)) $this->assertCapacity();
            $this->writeEvent($path, $channel_id, $generation, $event);
            $result = call_user_func($processor, $event);
            if (!is_array($result) || empty($result['success'])) {
                throw new \RuntimeException((string)($result['error'] ?? 'max_inbox_processor_failed'));
            }
            $this->files->delete($path);
            return $result;
        } finally {
            flock($lock, LOCK_UN);
            fclose($lock);
        }
    }

     
    public function drain(int $channel_id, callable $processor, int $limit = 50): array {
        $lock = $this->channelLock($channel_id);
        $processed = 0;
        $failed = 0;
        try {
            $paths = $this->eventPaths($channel_id);
            foreach (array_slice($paths, 0, max(1, min(200, $limit))) as $path) {
                try {
                    $snapshot = $this->readEvent($path, $channel_id);
                    $result = call_user_func($processor, $snapshot['event']);
                    if (!is_array($result) || empty($result['success'])) {
                        throw new \RuntimeException((string)($result['error'] ?? 'max_inbox_processor_failed'));
                    }
                    $this->files->delete($path);
                    $processed++;
                } catch (\Throwable $e) {
                    $failed++;
                     
                     
                    break;
                }
            }
            return ['processed' => $processed, 'remaining' => count($this->eventPaths($channel_id)), 'failed' => $failed];
        } finally {
            flock($lock, LOCK_UN);
            fclose($lock);
        }
    }

    public function stats(int $channel_id): array {
        $paths = $this->eventPaths($channel_id);
        $bytes = 0;
        $oldest = 0;
        foreach ($paths as $path) {
            $stat = SafeRuntime::call(static function () use ($path) { return stat($path); }, false);
            if (!is_array($stat)) continue;
            $bytes += max(0, (int)($stat['size'] ?? 0));
            $mtime = max(0, (int)($stat['mtime'] ?? 0));
            if ($mtime > 0 && ($oldest === 0 || $mtime < $oldest)) $oldest = $mtime;
        }
        return ['count' => count($paths), 'bytes' => $bytes, 'oldest_at' => $oldest];
    }

    private function writeEvent(string $path, int $channel_id, int $generation, array $event): void {
        $payload = json_encode([
            'format' => self::FORMAT,
            'channel_id' => $channel_id,
            'generation' => $generation,
            'received_at' => time(),
            'event' => $event,
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if (!is_string($payload) || strlen($payload) > self::MAX_EVENT_BYTES) throw new \RuntimeException('max_inbox_event_too_large');
        $this->files->writeAtomic($path, $this->cipher->encrypt($this->context($channel_id, $generation), $payload));
    }

    private function readEvent(string $path, int $channel_id): array {
        $name = basename($path);
        if (preg_match('/-g([1-9][0-9]{0,9})-t[0-9]{13}-[a-f0-9]{64}\.enc$/D', $name, $match) !== 1) {
            throw new \RuntimeException('max_inbox_file_name_invalid');
        }
        $generation = (int)$match[1];
        $envelope = $this->files->read($path, self::MAX_EVENT_BYTES * 2);
        $json = is_string($envelope) ? $this->cipher->decrypt($this->context($channel_id, $generation), $envelope) : '';
        $data = json_decode($json, true);
        if (!is_array($data) || array_keys($data) !== ['format', 'channel_id', 'generation', 'received_at', 'event']
            || (int)$data['format'] !== self::FORMAT || (int)$data['channel_id'] !== $channel_id
            || (int)$data['generation'] !== $generation || !is_int($data['received_at']) || !is_array($data['event'])) {
            throw new \RuntimeException('max_inbox_event_invalid');
        }
        $this->identity($data['event']);
        return $data;
    }

    private function identity(array $event): string {
        $message_id = trim((string)($event['external_message_id'] ?? ''));
        $chat_id = trim((string)($event['external_chat_id'] ?? ''));
        if ($message_id === '' || $chat_id === '' || strlen($message_id) > 255 || strlen($chat_id) > 255) {
            throw new \InvalidArgumentException('max_inbox_identity_invalid');
        }
        return hash('sha256', $chat_id . "\0" . $message_id);
    }

    private function assertCapacity(): void {
        $paths = glob(dirname($this->files->path('capacity.probe')) . DIRECTORY_SEPARATOR . 'event-*.enc') ?: [];
        if (count($paths) >= self::MAX_EVENTS) throw new \RuntimeException('max_inbox_capacity_reached');
        $bytes = 0;
        foreach ($paths as $path) {
            $size = SafeRuntime::call(static function () use ($path) { return filesize($path); }, false);
            if (is_int($size)) $bytes += max(0, $size);
            if ($bytes >= self::MAX_TOTAL_BYTES) throw new \RuntimeException('max_inbox_capacity_reached');
        }
    }

    private function eventPaths(int $channel_id): array {
        $prefix = 'event-c' . substr(hash('sha256', (string)$channel_id), 0, 20) . '-g';
        $paths = glob(dirname($this->files->path('events.probe')) . DIRECTORY_SEPARATOR . $prefix . '*.enc') ?: [];
        sort($paths, SORT_STRING);
        return $paths;
    }
    private function eventPath(int $channel_id, int $generation, string $identity): string {
        $channel_hash = substr(hash('sha256', (string)$channel_id), 0, 20);
        $directory = dirname($this->files->path('events.probe'));
        $existing = glob($directory . DIRECTORY_SEPARATOR . 'event-c' . $channel_hash . '-g*-t*-' . $identity . '.enc') ?: [];
        if ($existing) {
            sort($existing, SORT_STRING);
            return $existing[0];
        }
        $received_ms = (int)floor(microtime(true) * 1000);
        return $this->files->path('event-c' . $channel_hash . '-g' . $generation . '-t' . sprintf('%013d', $received_ms) . '-' . $identity . '.enc');
    }
    private function channelLock(int $channel_id) {
        $lock = $this->files->openLock($this->files->path('drain-c' . substr(hash('sha256', (string)$channel_id), 0, 20) . '.lock'));
        if (!is_resource($lock)) throw new \RuntimeException('max_inbox_lock_failed');
        return $lock;
    }
    private function context(int $channel_id, int $generation): string {
        return "max-inbox\0channel:" . $channel_id . "\0generation:" . $generation;
    }
}
