<?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 PDO;

class PdoSessionStorage implements SessionStorageInterface
{
    protected SerializerInterface $serializer;
    private PDO $db;
    private string $tablePrefix;

    public function __construct(PDO $db, string $tablePrefix = '', ?SerializerInterface $serializer = null)
    {
        $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        if ('sqlite' === $db->getAttribute(PDO::ATTR_DRIVER_NAME)) {
            $db->query('PRAGMA foreign_keys = ON');
        }

        $this->db = $db;
        $this->tablePrefix = $tablePrefix;
        $this->serializer = $serializer ?? new PhpSerializer();
    }

    public function store(ActiveSession $activeSession): void
    {
        $stmt = $this->db->prepare(
            <<< SQL
                        INSERT
                            INTO
                                {$this->tablePrefix}sessions (
                                    session_id,
                                    expires_at,
                                    session_data
                                )
                            VALUES (
                                :session_id,
                                :expires_at,
                                :session_data
                            )
                SQL
        );
        $stmt->bindValue(':session_id', SessionStorageInterface::ID_PREFIX . $activeSession->sessionId(), PDO::PARAM_STR);
        $stmt->bindValue(':expires_at', $activeSession->expiresAt()->format(DateTimeImmutable::ATOM), PDO::PARAM_STR);
        $stmt->bindValue(
            ':session_data',
            $this->serializer->serialize(
                array_merge(
                    $activeSession->sessionData(),
                    [
                        '__expires_at' => $activeSession->expiresAt()->format(DateTimeImmutable::ATOM),
                    ]
                )
            ),
            PDO::PARAM_STR
        );
        $stmt->execute();
    }

    public function retrieve(string $sessionId): ?ActiveSession
    {
        $stmt = $this->db->prepare(
            <<< SQL
                        SELECT
                            session_data
                        FROM
                            {$this->tablePrefix}sessions
                        WHERE
                            session_id = :session_id
                SQL
        );
        $stmt->bindValue(':session_id', SessionStorageInterface::ID_PREFIX . $sessionId, PDO::PARAM_STR);
        $stmt->execute();

        $qSessionData = $stmt->fetchColumn();
        if (!\is_string($qSessionData)) {
            return null;
        }
        if (null === $sessionData = $this->serializer->unserialize($qSessionData)) {
            // 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);
    }

    public function destroy(string $sessionId): void
    {
        $stmt = $this->db->prepare(
            <<< SQL
                        DELETE
                            FROM
                                {$this->tablePrefix}sessions
                            WHERE
                                session_id = :session_id
                SQL
        );
        $stmt->bindValue(':session_id', SessionStorageInterface::ID_PREFIX . $sessionId, PDO::PARAM_STR);
        $stmt->execute();
    }

    public function init(): void
    {
        $this->db->query(
            <<< SQL
                        CREATE TABLE IF NOT EXISTS
                            {$this->tablePrefix}sessions (
                                session_id VARCHAR(255) PRIMARY KEY,
                                expires_at VARCHAR(255) NOT NULL,
                                session_data TEXT NOT NULL
                            )
                SQL
        );
    }

    public function drop(): void
    {
        $this->db->query(
            <<< SQL
                        DROP TABLE IF EXISTS
                            {$this->tablePrefix}sessions
                SQL
        );
    }
}
