<?php

declare(strict_types=1);

namespace App\Administration\Infrastructure\Persistence\Redis;

use App\Administration\Domain\Exception\SessionNotFoundException;
use App\Administration\Domain\Model\RefreshToken\TokenFamilyId;
use App\Administration\Domain\Model\Session\DeviceInfo;
use App\Administration\Domain\Model\Session\Location;
use App\Administration\Domain\Model\Session\Session;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Domain\Repository\SessionRepository;
use App\Shared\Domain\Tenant\TenantId;

use function count;

use DateTimeImmutable;
use DateTimeInterface;
use Psr\Cache\CacheItemPoolInterface;

/**
 * Redis implementation of the sessions repository.
 *
 * Storage structure:
 * - Session data: session:{family_id} → session JSON data
 * - User index: user_sessions:{user_id} → set of family_ids
 *
 * @see Story 1.6 - Gestion des sessions
 */
final readonly class RedisSessionRepository implements SessionRepository
{
    private const string SESSION_PREFIX = 'session:';
    private const string USER_SESSIONS_PREFIX = 'user_sessions:';

    /**
     * Maximum TTL for user index (8 days).
     *
     * Must be >= the longest possible session TTL to ensure
     * all sessions are properly indexed.
     */
    private const int MAX_USER_INDEX_TTL = 691200;

    public function __construct(
        private CacheItemPoolInterface $sessionsCache,
    ) {
    }

    public function save(Session $session, int $ttlSeconds): void
    {
        // Save the session
        $sessionItem = $this->sessionsCache->getItem(self::SESSION_PREFIX . $session->familyId);
        $sessionItem->set($this->serialize($session));
        $sessionItem->expiresAfter($ttlSeconds);
        $this->sessionsCache->save($sessionItem);

        // Add to user index
        $userItem = $this->sessionsCache->getItem(self::USER_SESSIONS_PREFIX . $session->userId);

        /** @var list<string> $familyIds */
        $familyIds = $userItem->isHit() ? $userItem->get() : [];
        $familyIds[] = (string) $session->familyId;
        $userItem->set(array_values(array_unique($familyIds)));
        $userItem->expiresAfter(self::MAX_USER_INDEX_TTL);
        $this->sessionsCache->save($userItem);
    }

    public function getByFamilyId(TokenFamilyId $familyId): Session
    {
        $session = $this->findByFamilyId($familyId);

        if ($session === null) {
            throw new SessionNotFoundException($familyId);
        }

        return $session;
    }

    public function findByFamilyId(TokenFamilyId $familyId): ?Session
    {
        $item = $this->sessionsCache->getItem(self::SESSION_PREFIX . $familyId);

        if (!$item->isHit()) {
            return null;
        }

        /** @var array{family_id: string, user_id: string, tenant_id: string, device: string, browser: string, os: string, raw_user_agent: string, ip: string|null, country: string|null, city: string|null, created_at: string, last_activity_at: string} $data */
        $data = $item->get();

        return $this->deserialize($data);
    }

    public function findAllByUserId(UserId $userId): array
    {
        $userItem = $this->sessionsCache->getItem(self::USER_SESSIONS_PREFIX . $userId);

        if (!$userItem->isHit()) {
            return [];
        }

        /** @var list<string> $familyIds */
        $familyIds = $userItem->get();
        $sessions = [];
        $validFamilyIds = [];

        foreach ($familyIds as $familyIdStr) {
            $session = $this->findByFamilyId(TokenFamilyId::fromString($familyIdStr));
            if ($session !== null) {
                $sessions[] = $session;
                $validFamilyIds[] = $familyIdStr;
            }
        }

        // Clean up stale entries from user index
        if (count($validFamilyIds) !== count($familyIds)) {
            $userItem->set($validFamilyIds);
            $userItem->expiresAfter(self::MAX_USER_INDEX_TTL);
            $this->sessionsCache->save($userItem);
        }

        return $sessions;
    }

    public function delete(TokenFamilyId $familyId): void
    {
        // Find session to get userId for index cleanup
        $session = $this->findByFamilyId($familyId);

        // Delete the session
        $this->sessionsCache->deleteItem(self::SESSION_PREFIX . $familyId);

        // Remove from user index if session existed
        if ($session !== null) {
            $this->removeFromUserIndex($session->userId, $familyId);
        }
    }

    public function deleteAllExcept(UserId $userId, TokenFamilyId $exceptFamilyId): array
    {
        $userItem = $this->sessionsCache->getItem(self::USER_SESSIONS_PREFIX . $userId);

        if (!$userItem->isHit()) {
            return [];
        }

        /** @var list<string> $familyIds */
        $familyIds = $userItem->get();
        $deletedFamilyIds = [];

        foreach ($familyIds as $familyIdStr) {
            $familyId = TokenFamilyId::fromString($familyIdStr);

            if ($familyId->equals($exceptFamilyId)) {
                continue;
            }

            $this->sessionsCache->deleteItem(self::SESSION_PREFIX . $familyIdStr);
            $deletedFamilyIds[] = $familyId;
        }

        // Update user index to only contain the excepted session
        $userItem->set([(string) $exceptFamilyId]);
        $userItem->expiresAfter(self::MAX_USER_INDEX_TTL);
        $this->sessionsCache->save($userItem);

        return $deletedFamilyIds;
    }

    public function updateActivity(TokenFamilyId $familyId, DateTimeImmutable $at, int $ttlSeconds): void
    {
        $session = $this->findByFamilyId($familyId);

        if ($session === null) {
            return;
        }

        $updatedSession = $session->updateActivity($at);

        // Preserve TTL aligned with refresh token expiry
        $sessionItem = $this->sessionsCache->getItem(self::SESSION_PREFIX . $familyId);
        $sessionItem->set($this->serialize($updatedSession));
        $sessionItem->expiresAfter($ttlSeconds);
        $this->sessionsCache->save($sessionItem);
    }

    private function removeFromUserIndex(UserId $userId, TokenFamilyId $familyId): void
    {
        $userItem = $this->sessionsCache->getItem(self::USER_SESSIONS_PREFIX . $userId);

        if (!$userItem->isHit()) {
            return;
        }

        /** @var list<string> $familyIds */
        $familyIds = $userItem->get();
        $familyIds = array_values(array_filter(
            $familyIds,
            static fn (string $id) => $id !== (string) $familyId,
        ));

        if (count($familyIds) === 0) {
            $this->sessionsCache->deleteItem(self::USER_SESSIONS_PREFIX . $userId);
        } else {
            $userItem->set($familyIds);
            $userItem->expiresAfter(self::MAX_USER_INDEX_TTL);
            $this->sessionsCache->save($userItem);
        }
    }

    /**
     * @return array<string, mixed>
     */
    private function serialize(Session $session): array
    {
        return [
            'family_id' => (string) $session->familyId,
            'user_id' => (string) $session->userId,
            'tenant_id' => (string) $session->tenantId,
            'device' => $session->deviceInfo->device,
            'browser' => $session->deviceInfo->browser,
            'os' => $session->deviceInfo->os,
            'raw_user_agent' => $session->deviceInfo->rawUserAgent,
            'ip' => $session->location->ip,
            'country' => $session->location->country,
            'city' => $session->location->city,
            'created_at' => $session->createdAt->format(DateTimeInterface::ATOM),
            'last_activity_at' => $session->lastActivityAt->format(DateTimeInterface::ATOM),
        ];
    }

    /**
     * @param array{
     *     family_id: string,
     *     user_id: string,
     *     tenant_id: string,
     *     device: string,
     *     browser: string,
     *     os: string,
     *     raw_user_agent: string,
     *     ip: string|null,
     *     country: string|null,
     *     city: string|null,
     *     created_at: string,
     *     last_activity_at: string
     * } $data
     */
    private function deserialize(array $data): Session
    {
        return Session::reconstitute(
            familyId: TokenFamilyId::fromString($data['family_id']),
            userId: UserId::fromString($data['user_id']),
            tenantId: TenantId::fromString($data['tenant_id']),
            deviceInfo: DeviceInfo::reconstitute(
                device: $data['device'],
                browser: $data['browser'],
                os: $data['os'],
                rawUserAgent: $data['raw_user_agent'],
            ),
            location: Location::reconstitute(
                ip: $data['ip'],
                country: $data['country'],
                city: $data['city'],
            ),
            createdAt: new DateTimeImmutable($data['created_at']),
            lastActivityAt: new DateTimeImmutable($data['last_activity_at']),
        );
    }
}
