<?php
namespace EasyChat\Library\Messenger;

use EasyChat\Library\Security\AeadEnvelopeCipher;
use EasyChat\Library\Security\PrivateFileArea;
use PHPMax\Session\SessionInfo;
use PHPMax\Session\SessionStoreInterface;





final class MaxEncryptedSessionStore implements SessionStoreInterface {
    private const FORMAT = 2;
    private const MAX_BYTES = 2097152;
    private $files;
    private $cipher;
    private $channel_id;
    private $generation;
    private $base;
    private $recovered = false;

    public function __construct(PrivateFileArea $files, AeadEnvelopeCipher $cipher, int $channel_id, int $generation) {
        if ($channel_id <= 0 || $generation <= 0) throw new \InvalidArgumentException('max_session_identity_invalid');
        $this->files = $files;
        $this->cipher = $cipher;
        $this->channel_id = $channel_id;
        $this->generation = $generation;
        $this->base = 'channel-' . hash('sha256', (string)$channel_id);
    }

    public function saveSession(SessionInfo $sessionInfo): void {
        $this->withLock(function () use ($sessionInfo): void {
            $snapshot = $this->loadSnapshotLocked(true);
            $revision = max(0, (int)($snapshot['revision'] ?? 0)) + 1;
            $payload = $this->encodeSnapshot($revision, [$sessionInfo->toArray()]);
            $current = $this->files->read($this->currentPath(), self::MAX_BYTES);
            if (is_string($current)) {
                 
                $this->decodeSnapshot($current);
                $this->files->writeAtomic($this->previousPath(), $current);
            }
            $this->files->writeAtomic($this->currentPath(), $payload);
            $this->decodeSnapshot((string)$this->files->read($this->currentPath(), self::MAX_BYTES));
        });
    }

     
    public function replaceSession(SessionInfo $sessionInfo): void {
        $this->withLock(function () use ($sessionInfo): void {
            $payload = $this->encodeSnapshot(1, [$sessionInfo->toArray()]);
            $this->files->writeAtomic($this->currentPath(), $payload);
            $this->decodeSnapshot((string)$this->files->read($this->currentPath(), self::MAX_BYTES));
            $this->files->delete($this->previousPath());
        });
    }

    public function updateToken(string $oldToken, string $newToken): void {
        $session = $this->loadSession();
        if (!$session instanceof SessionInfo || !hash_equals((string)$session->token, $oldToken)) return;
        $this->saveSession(new SessionInfo([
            'token' => $newToken,
            'deviceId' => $session->deviceId,
            'phone' => $session->phone,
            'mtInstanceId' => $session->mtInstanceId,
            'sync' => $session->sync,
        ]));
    }

    public function loadSession(): ?SessionInfo {
        return $this->withLock(function (): ?SessionInfo {
            $snapshot = $this->loadSnapshotLocked(true);
            $sessions = $snapshot['sessions'] ?? [];
            return $sessions ? SessionInfo::fromArray($sessions[0]) : null;
        });
    }
    public function loadSessionByDeviceId(string $deviceId): ?SessionInfo {
        $session = $this->loadSession();
        return $session instanceof SessionInfo && hash_equals((string)$session->deviceId, $deviceId) ? $session : null;
    }
    public function loadSessionByPhone(string $phone): ?SessionInfo {
        $session = $this->loadSession();
        return $session instanceof SessionInfo && hash_equals((string)$session->phone, $phone) ? $session : null;
    }
    public function deleteSession(string $token): void {
        $session = $this->loadSession();
        if ($session instanceof SessionInfo && hash_equals((string)$session->token, $token)) $this->deleteAllSessions();
    }
    public function deleteAllSessions(): void {
        $this->withLock(function (): void {
            $this->files->delete($this->currentPath());
            $this->files->delete($this->previousPath());
        });
    }
    public function close(): void {}
    public function recoveredPrevious(): bool { return $this->recovered; }

    public function inspect(): array {
        try {
            $session = $this->loadSession();
            return [
                'status' => $session instanceof SessionInfo ? 'ready' : 'missing',
                'session_count' => $session instanceof SessionInfo ? 1 : 0,
                'legacy_multiple' => false,
                'recovered_previous' => $this->recovered,
                'encrypted' => true,
            ];
        } catch (\Throwable $e) {
            return ['status' => 'invalid', 'session_count' => 0, 'legacy_multiple' => false, 'recovered_previous' => false, 'encrypted' => true];
        }
    }

    private function loadSnapshotLocked(bool $recover): array {
        $current = $this->files->read($this->currentPath(), self::MAX_BYTES);
        if ($current === null) return ['revision' => 0, 'sessions' => []];
        try {
            return $this->decodeSnapshot($current);
        } catch (\Throwable $current_error) {
            if (!$recover) throw $current_error;
            $previous = $this->files->read($this->previousPath(), self::MAX_BYTES);
            if ($previous === null) throw $current_error;
            $snapshot = $this->decodeSnapshot($previous);
            $this->files->writeAtomic($this->currentPath(), $previous);
            $this->recovered = true;
            return $snapshot;
        }
    }

    private function encodeSnapshot(int $revision, array $sessions): string {
        $json = json_encode([
            'format' => self::FORMAT,
            'channel_hash' => hash('sha256', (string)$this->channel_id),
            'generation' => $this->generation,
            'revision' => $revision,
            'sessions' => $sessions,
        ], JSON_UNESCAPED_SLASHES);
        if (!is_string($json) || strlen($json) > 1048576) throw new \RuntimeException('max_session_encode_failed');
        return $this->cipher->encrypt($this->context(), $json);
    }

    private function decodeSnapshot(string $envelope): array {
        $json = $this->cipher->decrypt($this->context(), $envelope);
        $data = json_decode($json, true);
        if (!is_array($data) || array_keys($data) !== ['format', 'channel_hash', 'generation', 'revision', 'sessions']
            || (int)$data['format'] !== self::FORMAT
            || !is_string($data['channel_hash']) || !hash_equals(hash('sha256', (string)$this->channel_id), $data['channel_hash'])
            || (int)$data['generation'] !== $this->generation
            || !is_int($data['revision']) || $data['revision'] < 1
            || !is_array($data['sessions']) || count($data['sessions']) > 1) {
            throw new \RuntimeException('max_session_snapshot_invalid');
        }
        if ($data['sessions'] && !is_array($data['sessions'][0])) throw new \RuntimeException('max_session_snapshot_invalid');
        return ['revision' => $data['revision'], 'sessions' => $data['sessions']];
    }

    private function withLock(callable $callback) {
        $lock = $this->files->openLock($this->files->path($this->base . '.lock'));
        if (!is_resource($lock)) throw new \RuntimeException('max_session_store_lock_failed');
        try { return $callback(); }
        finally { flock($lock, LOCK_UN); fclose($lock); }
    }
    private function currentPath(): string { return $this->files->path($this->base . '.current.enc'); }
    private function previousPath(): string { return $this->files->path($this->base . '.previous.enc'); }
    private function context(): string { return "max-session\0channel:" . $this->channel_id . "\0generation:" . $this->generation; }
}
