<?php
namespace EasyChat\Library\Messenger;

use PHPMax\Session\SessionInfo;
use PHPMax\Session\SessionStoreInterface;

class MaxSessionGenerationChangedException extends \RuntimeException {
}

class MaxSessionLifecycleBusyException extends \RuntimeException {
}

class MaxSessionAuthorizationRequiredException extends \RuntimeException {
}








class MaxGenerationSessionStore implements SessionStoreInterface {

    private const LOCK_TIMEOUT_MS = 5000;
    private const LOCK_RETRY_US = 50000;

    private $inner;
    private $lock_path;
    private $expected_generation;
    private $generation_reader;

    public function __construct(
        SessionStoreInterface $inner,
        string $lock_path,
        int $expected_generation,
        callable $generation_reader
    ) {
        $this->inner = $inner;
        $this->lock_path = $lock_path;
        $this->expected_generation = max(1, $expected_generation);
        $this->generation_reader = $generation_reader;
    }

    public function saveSession(SessionInfo $sessionInfo): void {
        $this->withCurrentGeneration(function () use ($sessionInfo): void {
            $this->inner->saveSession($sessionInfo);
        }, true);
    }

    public function updateToken(string $oldToken, string $newToken): void {
        $this->withCurrentGeneration(function () use ($oldToken, $newToken): void {
            $this->inner->updateToken($oldToken, $newToken);
        }, true);
    }

    public function loadSession(): ?SessionInfo {
        return $this->withCurrentGeneration(function (): ?SessionInfo {
            return $this->inner->loadSession();
        });
    }

    public function loadSessionByDeviceId(string $deviceId): ?SessionInfo {
        return $this->withCurrentGeneration(function () use ($deviceId): ?SessionInfo {
            return $this->inner->loadSessionByDeviceId($deviceId);
        });
    }

    public function loadSessionByPhone(string $phone): ?SessionInfo {
        return $this->withCurrentGeneration(function () use ($phone): ?SessionInfo {
            return $this->inner->loadSessionByPhone($phone);
        });
    }

    public function deleteSession(string $token): void {
        $this->withCurrentGeneration(function () use ($token): void {
            $this->inner->deleteSession($token);
        }, true);
    }

    public function deleteAllSessions(): void {
        $this->withCurrentGeneration(function (): void {
            $this->inner->deleteAllSessions();
        }, true);
    }

    public function close(): void {
        $this->inner->close();
    }

    private function withCurrentGeneration(callable $callback, bool $exclusive = false) {
        $handle = $this->openLock();
        $locked = false;
        try {
            $locked = $this->acquireLock($handle, $exclusive ? LOCK_EX : LOCK_SH);

            $current = max(1, (int)call_user_func($this->generation_reader));
            if ($current !== $this->expected_generation) {
                throw new MaxSessionGenerationChangedException('MAX session generation changed');
            }

            return call_user_func($callback);
        } finally {
            if (is_resource($handle)) {
                if ($locked) {
                    flock($handle, LOCK_UN);
                }
                fclose($handle);
            }
        }
    }

    private function acquireLock($handle, int $mode): bool {
        $deadline = microtime(true) + (self::LOCK_TIMEOUT_MS / 1000);
        do {
            if (flock($handle, $mode | LOCK_NB)) {
                return true;
            }
            usleep(self::LOCK_RETRY_US);
        } while (microtime(true) < $deadline);

        throw new MaxSessionLifecycleBusyException('MAX session lifecycle lock timed out');
    }

    private function openLock() {
        $dir = dirname($this->lock_path);
        if (!is_dir($dir) && !mkdir($dir, 0700, true) && !is_dir($dir)) {
            throw new MaxSessionLifecycleBusyException('MAX session lifecycle directory is unavailable');
        }

        $handle = MaxPrivateFileLock::open($this->lock_path);
        if (!is_resource($handle)) {
            throw new MaxSessionLifecycleBusyException('MAX session lifecycle lock cannot be opened');
        }
        return $handle;
    }
}
