<?php

declare(strict_types=1);

namespace App\Administration\Infrastructure\Persistence\Cache;

use App\Administration\Domain\Exception\UserNotFoundException;
use App\Administration\Domain\Model\ConsentementParental\ConsentementParental;
use App\Administration\Domain\Model\User\Email;
use App\Administration\Domain\Model\User\Role;
use App\Administration\Domain\Model\User\StatutCompte;
use App\Administration\Domain\Model\User\User;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Domain\Repository\UserRepository;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;

use function in_array;

use Override;
use Psr\Cache\CacheItemPoolInterface;
use RuntimeException;

use function sprintf;

/**
 * Cache-based UserRepository for development and testing.
 * Uses PSR-6 cache (filesystem in dev, Redis in prod).
 *
 * Note: Uses a dedicated users.cache pool with no TTL to ensure
 * user records don't expire (unlike activation tokens which expire after 7 days).
 */
final readonly class CacheUserRepository implements UserRepository
{
    private const string KEY_PREFIX = 'user:';
    private const string EMAIL_INDEX_PREFIX = 'user_email:';
    private const string TENANT_INDEX_PREFIX = 'user_tenant:';

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

    public function save(User $user): void
    {
        // Save user data
        $item = $this->usersCache->getItem(self::KEY_PREFIX . $user->id);
        $item->set($this->serialize($user));
        $this->usersCache->save($item);

        // Save email index for lookup (scoped to tenant)
        $emailKey = $this->emailIndexKey($user->email, $user->tenantId);
        $emailItem = $this->usersCache->getItem($emailKey);
        $emailItem->set((string) $user->id);
        $this->usersCache->save($emailItem);

        // Save tenant index for listing users
        $tenantKey = self::TENANT_INDEX_PREFIX . $user->tenantId;
        $tenantItem = $this->usersCache->getItem($tenantKey);
        /** @var string[] $userIds */
        $userIds = $tenantItem->isHit() ? $tenantItem->get() : [];
        $userId = (string) $user->id;
        if (!in_array($userId, $userIds, true)) {
            $userIds[] = $userId;
        }
        $tenantItem->set($userIds);
        $this->usersCache->save($tenantItem);
    }

    #[Override]
    public function findById(UserId $id): ?User
    {
        $item = $this->usersCache->getItem(self::KEY_PREFIX . $id);

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

        /** @var array{id: string, email: string, roles?: string[], role?: string, tenant_id: string, school_name: string, statut: string, hashed_password: string|null, date_naissance: string|null, created_at: string, activated_at: string|null, first_name?: string, last_name?: string, invited_at?: string|null, blocked_at?: string|null, blocked_reason?: string|null, consentement_parental: array{parent_id: string, eleve_id: string, date_consentement: string, ip_address: string}|null} $data */
        $data = $item->get();

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

    public function findByEmail(Email $email, TenantId $tenantId): ?User
    {
        $emailKey = $this->emailIndexKey($email, $tenantId);
        $emailItem = $this->usersCache->getItem($emailKey);

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

        /** @var string $userId */
        $userId = $emailItem->get();

        return $this->findById(UserId::fromString($userId));
    }

    public function get(UserId $id): User
    {
        $user = $this->findById($id);

        if ($user === null) {
            throw UserNotFoundException::withId($id);
        }

        return $user;
    }

    public function findAllByTenant(TenantId $tenantId): array
    {
        $tenantKey = self::TENANT_INDEX_PREFIX . $tenantId;
        $tenantItem = $this->usersCache->getItem($tenantKey);

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

        /** @var string[] $userIds */
        $userIds = $tenantItem->get();
        $users = [];

        foreach ($userIds as $userId) {
            $user = $this->findById(UserId::fromString($userId));
            if ($user !== null) {
                $users[] = $user;
            }
        }

        return $users;
    }

    /**
     * @return array<string, mixed>
     */
    private function serialize(User $user): array
    {
        $consentement = $user->consentementParental;

        return [
            'id' => (string) $user->id,
            'email' => (string) $user->email,
            'roles' => array_map(static fn (Role $r) => $r->value, $user->roles),
            'tenant_id' => (string) $user->tenantId,
            'school_name' => $user->schoolName,
            'statut' => $user->statut->value,
            'hashed_password' => $user->hashedPassword,
            'date_naissance' => $user->dateNaissance?->format('Y-m-d'),
            'created_at' => $user->createdAt->format('c'),
            'activated_at' => $user->activatedAt?->format('c'),
            'first_name' => $user->firstName,
            'last_name' => $user->lastName,
            'invited_at' => $user->invitedAt?->format('c'),
            'blocked_at' => $user->blockedAt?->format('c'),
            'blocked_reason' => $user->blockedReason,
            'consentement_parental' => $consentement !== null ? [
                'parent_id' => $consentement->parentId,
                'eleve_id' => $consentement->eleveId,
                'date_consentement' => $consentement->dateConsentement->format('c'),
                'ip_address' => $consentement->ipAddress,
            ] : null,
        ];
    }

    /**
     * @param array{
     *     id: string,
     *     email: string,
     *     roles?: string[],
     *     role?: string,
     *     tenant_id: string,
     *     school_name: string,
     *     statut: string,
     *     hashed_password: string|null,
     *     date_naissance: string|null,
     *     created_at: string,
     *     activated_at: string|null,
     *     first_name?: string,
     *     last_name?: string,
     *     invited_at?: string|null,
     *     blocked_at?: string|null,
     *     blocked_reason?: string|null,
     *     consentement_parental: array{parent_id: string, eleve_id: string, date_consentement: string, ip_address: string}|null
     * } $data
     */
    private function deserialize(array $data): User
    {
        $consentement = null;
        if ($data['consentement_parental'] !== null) {
            $consentementData = $data['consentement_parental'];
            $consentement = ConsentementParental::accorder(
                parentId: $consentementData['parent_id'],
                eleveId: $consentementData['eleve_id'],
                at: new DateTimeImmutable($consentementData['date_consentement']),
                ipAddress: $consentementData['ip_address'],
            );
        }

        $invitedAt = ($data['invited_at'] ?? null) !== null ? new DateTimeImmutable($data['invited_at']) : null;
        $blockedAt = ($data['blocked_at'] ?? null) !== null ? new DateTimeImmutable($data['blocked_at']) : null;

        // Support both legacy single role and new multi-role format
        $roleStrings = $data['roles'] ?? (isset($data['role']) ? [$data['role']] : []);

        if ($roleStrings === []) {
            throw new RuntimeException(sprintf('User %s has no roles in cache data.', $data['id']));
        }

        $roles = array_map(static fn (string $r) => Role::from($r), $roleStrings);

        return User::reconstitute(
            id: UserId::fromString($data['id']),
            email: new Email($data['email']),
            roles: $roles,
            tenantId: TenantId::fromString($data['tenant_id']),
            schoolName: $data['school_name'],
            statut: StatutCompte::from($data['statut']),
            dateNaissance: $data['date_naissance'] !== null ? new DateTimeImmutable($data['date_naissance']) : null,
            createdAt: new DateTimeImmutable($data['created_at']),
            hashedPassword: $data['hashed_password'],
            activatedAt: $data['activated_at'] !== null ? new DateTimeImmutable($data['activated_at']) : null,
            consentementParental: $consentement,
            firstName: $data['first_name'] ?? '',
            lastName: $data['last_name'] ?? '',
            invitedAt: $invitedAt,
            blockedAt: $blockedAt,
            blockedReason: $data['blocked_reason'] ?? null,
        );
    }

    private function normalizeEmail(Email $email): string
    {
        return strtolower(str_replace(['@', '.'], ['_at_', '_dot_'], (string) $email));
    }

    /**
     * Creates a cache key for email lookup scoped to a tenant.
     */
    private function emailIndexKey(Email $email, TenantId $tenantId): string
    {
        return self::EMAIL_INDEX_PREFIX . $tenantId . ':' . $this->normalizeEmail($email);
    }
}
