<?php

declare(strict_types=1);

namespace Ekdos\Auth;

use Ekdos\Users\User;
use Redis;

/**
 * Serverseitiger Sitzungsspeicher (BFF-Ticket-Store) auf Redis.
 *
 * Der Browser bekommt ausschliesslich eine zufaellige, undurchsichtige Kennung.
 * Rollen, Rechte und Anzeigename bleiben auf dem Server: das Cookie waechst
 * nicht mit den Claims mit und ein Reverse Proxy hat nie zu grosse Header.
 *
 * Schluessel:
 *   <prefix>sess:<id>        Sitzungsdaten, TTL = Leerlauffenster
 *   <prefix>sess:user:<uid>  Menge aller Sitzungen eines Benutzers, fuer Zwangsabmeldung
 */
final readonly class SessionStore
{
    public function __construct(
        private Redis $redis,
        private string $prefix,
        private int $ttl,
    ) {}

    public function create(User $user): Session
    {
        $now = time();
        $session = new Session(
            id: self::newId(),
            userId: $user->id,
            username: $user->username,
            displayName: $user->displayName,
            role: $user->role,
            issuedAt: $now,
            lastSeenAt: $now,
        );

        $this->redis->setex($this->key($session->id), $this->ttl, json_encode($session->toArray(), JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
        $this->redis->sAdd($this->userKey($user->id), $session->id);
        // Der Index darf den laengsten moeglichen Sitzungslauf ueberdauern, nicht laenger.
        $this->redis->expire($this->userKey($user->id), $this->ttl * 24);

        return $session;
    }

    public function read(string $id): ?Session
    {
        if (!self::looksLikeId($id)) {
            return null;
        }

        $raw = $this->redis->get($this->key($id));

        if (!is_string($raw) || $raw === '') {
            return null;
        }

        $decoded = json_decode($raw, true);

        return is_array($decoded) ? Session::fromArray($id, $decoded) : null;
    }

    /**
     * Gleitendes Leerlauffenster: jede authentifizierte Anfrage schiebt die TTL nach vorn.
     * Der Rumpf wird nur einmal pro Minute neu geschrieben, sonst reicht ein EXPIRE.
     */
    public function touch(Session $session): void
    {
        $now = time();

        if ($now - $session->lastSeenAt < 60) {
            $this->redis->expire($this->key($session->id), $this->ttl);

            return;
        }

        $refreshed = new Session(
            id: $session->id,
            userId: $session->userId,
            username: $session->username,
            displayName: $session->displayName,
            role: $session->role,
            issuedAt: $session->issuedAt,
            lastSeenAt: $now,
        );

        $this->redis->setex($this->key($session->id), $this->ttl, json_encode($refreshed->toArray(), JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
    }

    public function destroy(string $id): void
    {
        $session = $this->read($id);

        if ($session !== null) {
            $this->redis->sRem($this->userKey($session->userId), $id);
        }

        $this->redis->del($this->key($id));
    }

    /**
     * Meldet einen Benutzer ueberall ab. Wird nach Rollenwechsel, Deaktivierung,
     * Passwortwechsel und Loeschung aufgerufen, damit eine laufende Sitzung
     * keine Rechte behaelt, die der Benutzer nicht mehr hat.
     */
    public function destroyAllFor(string $userId): int
    {
        $ids = $this->redis->sMembers($this->userKey($userId));

        if (!is_array($ids) || $ids === []) {
            $this->redis->del($this->userKey($userId));

            return 0;
        }

        $this->redis->del(array_map(fn (string $id): string => $this->key($id), $ids));
        $this->redis->del($this->userKey($userId));

        return count($ids);
    }

    private static function newId(): string
    {
        return rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
    }

    private static function looksLikeId(string $id): bool
    {
        return $id !== '' && strlen($id) <= 64 && preg_match('/^[A-Za-z0-9_-]+$/', $id) === 1;
    }

    private function key(string $id): string
    {
        return $this->prefix . 'sess:' . $id;
    }

    private function userKey(string $userId): string
    {
        return $this->prefix . 'sess:user:' . $userId;
    }
}
