<?php

/**
 * @package Dotclear
 *
 * @copyright Olivier Meunier & Association Dotclear
 * @copyright AGPL-3.0
 */
declare(strict_types=1);

namespace Dotclear\Database;

use Dotclear\Database\Statement\DeleteStatement;
use Dotclear\Database\Statement\SelectStatement;
use Dotclear\Database\Statement\UpdateStatement;
use Dotclear\Interface\Database\ConnectionInterface;
use SessionHandlerInterface;

/**
 * @class SessionHandler
 *
 * Database Session Handler
 *
 * This class allows you to handle session data in database.
 */
class SessionHandler implements SessionHandlerInterface
{
    /**
     * Constructor
     *
     * This method creates an instance of Session class.
     *
     * @param ConnectionInterface   $con               AbstractHandler inherited database instance
     * @param string                $table             Table name
     * @param string                $ttl               TTL
     */
    public function __construct(
        private readonly ConnectionInterface $con,
        private readonly string $table,
        private string $ttl
    ) {
        if (!str_starts_with(trim($this->ttl), '-')) {
            // We will use negative session TTL
            $this->ttl = '-' . trim($this->ttl);
        }
    }

    /**
     * Session handler callback called on session open
     *
     * @param      string  $path   The save path
     * @param      string  $name   The session name
     */
    public function open(string $path, string $name): bool
    {
        return true;
    }

    /**
     * Session handler callback called on session close
     */
    public function close(): bool
    {
        $this->gc(0);

        return true;
    }

    /**
     * Session handler callback called on session read
     *
     * @param      string  $id  The session identifier
     */
    public function read(string $id): string
    {
        $sql = new SelectStatement($this->con, $this->con->syntax());
        $sql
            ->field('ses_value')
            ->from($this->table)
            ->where('ses_id = ' . $sql->quote($this->checkID($id)))
        ;

        $rs = $sql->select();
        if (!$rs instanceof MetaRecord || $rs->isEmpty()) {
            return '';
        }

        return $rs->strField('ses_value');
    }

    /**
     * Session handler callback called on session write
     *
     * @param      string  $id      The session identifier
     * @param      string  $data    The data
     */
    public function write(string $id, string $data): bool
    {
        $sql = new SelectStatement($this->con, $this->con->syntax());
        $sql
            ->field('ses_id')
            ->from($this->table)
            ->where('ses_id = ' . $sql->quote($this->checkID($id)))
        ;

        $rs = $sql->select();
        if ($rs instanceof MetaRecord) {
            $cur = $this->con->openCursor($this->table);
            $cur->setStrField('ses_time', (string) time());
            $cur->setStrField('ses_value', $data);

            if (!$rs->isEmpty()) {
                $sqlUpdate = new UpdateStatement($this->con, $this->con->syntax());
                $sqlUpdate
                    ->where('ses_id = ' . $sqlUpdate->quote($this->checkID($id)))
                    ->update($cur)
                ;
            } else {
                $cur->setStrField('ses_id', $this->checkID($id));
                $cur->setStrField('ses_start', (string) time());

                $cur->insert();
            }

            return true;
        }

        return false;
    }

    /**
     * Session handler callback called on session destroy
     *
     * @param      string  $id  The session identifier
     */
    public function destroy(string $id): bool
    {
        $sql = new DeleteStatement($this->con, $this->con->syntax());
        $sql
            ->from($this->table)
            ->where('ses_id = ' . $sql->quote($this->checkID($id)))
        ;
        $sql->delete();

        $this->optimize();

        return true;
    }

    /**
     * Session handler callback called on session garbage collect
     */
    public function gc(int $max_lifetime): int
    {
        $ses_life = (int) strtotime($this->ttl);

        $sql = new DeleteStatement($this->con, $this->con->syntax());
        $sql
            ->from($this->table)
            ->where('ses_time < ' . $ses_life)
        ;
        $sql->delete();

        if ($this->con->changes() > 0) {
            $this->optimize();
        }

        return (int) true;
    }

    /**
     * Optimize the session table
     */
    private function optimize(): void
    {
        $this->con->vacuum($this->table);
    }

    /**
     * Check a session id
     *
     * @param      string  $id     The identifier
     */
    private function checkID(string $id): string
    {
        return preg_match('/^([0-9a-f]{40})$/i', $id) ? $id : '';
    }
}
