<?php

declare(strict_types=1);

/*
 * Copyright (c) 2017-2022 François Kooman <fkooman@tuxed.net>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

namespace fkooman\SeCookie;

use DateTimeImmutable;
use fkooman\SeCookie\Exception\SessionException;
use Memcached;

/**
 * Store session data in memcached using the PHP memcached extension.
 */
class MemcacheSessionStorage implements SessionStorageInterface
{
    /** @var array<string> */
    private array $memcacheServerList;

    private SerializerInterface $serializer;

    /**
     * @param array<string> $memcacheServerList
     */
    public function __construct(array $memcacheServerList, ?SerializerInterface $serializer = null)
    {
        if (!\extension_loaded('memcached')) {
            throw new SessionException('"memcached" PHP extension not available');
        }
        $this->memcacheServerList = $memcacheServerList;
        $this->serializer = $serializer ?? new PhpSerializer();
    }

    public function store(ActiveSession $activeSession): void
    {
        $sessionId = $activeSession->sessionId();
        $sessionDataString = $this->serializer->serialize(
            array_merge(
                $activeSession->sessionData(),
                [
                    '__expires_at' => $activeSession->expiresAt()->format(DateTimeImmutable::ATOM),
                ],
            )
        );

        $successCount = 0;
        foreach ($this->memcacheServerList as $memcacheServer) {
            $m = self::memcacheInstance($memcacheServer);
            if ($m->set(SessionStorageInterface::ID_PREFIX . $sessionId, $sessionDataString)) {
                ++$successCount;
            }
            $m->quit();
        }

        if (0 === $successCount) {
            throw new SessionException('unable to write session data to any of the memcache servers');
        }
    }

    public function retrieve(string $sessionId): ?ActiveSession
    {
        foreach ($this->memcacheServerList as $memcacheServer) {
            $m = self::memcacheInstance($memcacheServer);

            /** @var mixed */
            $cacheResponse = $m->get(SessionStorageInterface::ID_PREFIX . $sessionId);
            $m->quit();

            if (!\is_string($cacheResponse)) {
                continue;
            }

            if (null === $sessionData = $this->serializer->unserialize($cacheResponse)) {
                // we interprete corrupt session data as no session
                $this->destroy($sessionId);

                return null;
            }
            if (!\array_key_exists('__expires_at', $sessionData) || !\is_string($sessionData['__expires_at'])) {
                return null;
            }
            $expiresAt = new DateTimeImmutable($sessionData['__expires_at']);
            // we do not need __expires_at in sessionData, it is part of the object
            unset($sessionData['__expires_at']);

            return new ActiveSession($sessionId, $expiresAt, $sessionData);
        }

        return null;
    }

    public function destroy(string $sessionId): void
    {
        foreach ($this->memcacheServerList as $memcacheServer) {
            $m = self::memcacheInstance($memcacheServer);
            $m->delete(SessionStorageInterface::ID_PREFIX . $sessionId);
            $m->quit();
        }
    }

    private static function memcacheInstance(string $memcacheServer): Memcached
    {
        $serverInfo = explode(':', $memcacheServer, 2);
        if (2 !== count($serverInfo)) {
            throw new SessionException('invalid memcache instance, format: `host:port`');
        }
        [$memcacheHost, $memcachePort] = $serverInfo;
        $m = new Memcached();
        $m->addServer($memcacheHost, (int) $memcachePort);

        return $m;
    }
}
