<?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\ImageRightsStatus;
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 is_string;

use Override;
use Psr\Cache\CacheItemPoolInterface;
use Throwable;

/**
 * Cache-aside decorator around DoctrineUserRepository.
 *
 * Reads: Redis first, miss → PostgreSQL → populate Redis.
 * Writes: PostgreSQL first (source of truth) → update Redis.
 * If Redis is unavailable, falls back to PostgreSQL silently.
 */
final readonly class CachedUserRepository implements UserRepository
{
    private const string KEY_PREFIX = 'user:';
    private const string EMAIL_INDEX_PREFIX = 'user_email:';

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

    #[Override]
    public function save(User $user): void
    {
        // 1. PostgreSQL first (source of truth)
        $this->inner->save($user);

        // 2. Update Redis cache
        try {
            // Invalidate stale email index if the email changed
            $existingItem = $this->usersCache->getItem(self::KEY_PREFIX . $user->id);
            if ($existingItem->isHit()) {
                /** @var array<string, mixed> $oldData */
                $oldData = $existingItem->get();
                /** @var string $oldEmail */
                /** @var string|null $oldEmail */
                $oldEmail = $oldData['email'] ?? null;
                $currentEmail = $user->email !== null ? (string) $user->email : null;
                if ($oldEmail !== null && $oldEmail !== '' && $oldEmail !== $currentEmail) {
                    /** @var string $oldTenantId */
                    $oldTenantId = $oldData['tenant_id'] ?? (string) $user->tenantId;
                    $oldEmailKey = $this->emailIndexKey(
                        new Email($oldEmail),
                        TenantId::fromString($oldTenantId),
                    );
                    $this->usersCache->deleteItem($oldEmailKey);
                }
            }

            $existingItem->set($this->serialize($user));
            $this->usersCache->save($existingItem);

            // Email index
            if ($user->email !== null) {
                $emailKey = $this->emailIndexKey($user->email, $user->tenantId);
                $emailItem = $this->usersCache->getItem($emailKey);
                $emailItem->set((string) $user->id);
                $this->usersCache->save($emailItem);
            }
        } catch (Throwable) {
            // Redis unavailable — PostgreSQL write succeeded, data is safe
        }
    }

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

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

        return $user;
    }

    #[Override]
    public function findById(UserId $id): ?User
    {
        // 1. Try Redis
        try {
            $item = $this->usersCache->getItem(self::KEY_PREFIX . $id);
            if ($item->isHit()) {
                /** @var array<string, mixed> $data */
                $data = $item->get();

                return $this->deserialize($data);
            }
        } catch (Throwable) {
            // Redis unavailable, continue to PostgreSQL
        }

        // 2. Fallback PostgreSQL
        $user = $this->inner->findById($id);

        // 3. Populate cache
        if ($user !== null) {
            $this->populateCache($user);
        }

        return $user;
    }

    #[Override]
    public function findByEmail(Email $email, TenantId $tenantId): ?User
    {
        // 1. Try Redis email index
        try {
            $emailKey = $this->emailIndexKey($email, $tenantId);
            $emailItem = $this->usersCache->getItem($emailKey);

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

                return $this->findById(UserId::fromString($userId));
            }
        } catch (Throwable) {
            // Redis unavailable, continue to PostgreSQL
        }

        // 2. Fallback PostgreSQL
        $user = $this->inner->findByEmail($email, $tenantId);

        // 3. Populate cache
        if ($user !== null) {
            $this->populateCache($user);
        }

        return $user;
    }

    #[Override]
    public function findAllByTenant(TenantId $tenantId): array
    {
        // Always go to PostgreSQL for full list (cache may be incomplete)
        $users = $this->inner->findAllByTenant($tenantId);

        // Populate cache for each user
        foreach ($users as $user) {
            $this->populateCache($user);
        }

        return $users;
    }

    #[Override]
    public function findStudentsByTenant(TenantId $tenantId): array
    {
        return $this->inner->findStudentsByTenant($tenantId);
    }

    private function populateCache(User $user): void
    {
        try {
            $item = $this->usersCache->getItem(self::KEY_PREFIX . $user->id);
            $item->set($this->serialize($user));
            $this->usersCache->save($item);

            // Email index
            if ($user->email !== null) {
                $emailKey = $this->emailIndexKey($user->email, $user->tenantId);
                $emailItem = $this->usersCache->getItem($emailKey);
                $emailItem->set((string) $user->id);
                $this->usersCache->save($emailItem);
            }
        } catch (Throwable) {
            // Redis unavailable
        }
    }

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

        return [
            'id' => (string) $user->id,
            'email' => $user->email !== null ? (string) $user->email : null,
            '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,
            'image_rights_status' => $user->imageRightsStatus->value,
            'image_rights_updated_at' => $user->imageRightsUpdatedAt?->format('c'),
            'image_rights_updated_by' => $user->imageRightsUpdatedBy !== null ? (string) $user->imageRightsUpdatedBy : null,
            'student_number' => $user->studentNumber,
            '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<string, mixed> $data
     */
    private function deserialize(array $data): User
    {
        /** @var string $id */
        $id = $data['id'];
        /** @var string|null $email */
        $email = $data['email'];
        // Support both legacy single role ('role') and multi-role ('roles') format
        /** @var string[] $roleStrings */
        $roleStrings = $data['roles'] ?? (isset($data['role']) ? [$data['role']] : []);
        /** @var string $tenantId */
        $tenantId = $data['tenant_id'];
        /** @var string $schoolName */
        $schoolName = $data['school_name'];
        /** @var string $statut */
        $statut = $data['statut'];
        /** @var string|null $hashedPassword */
        $hashedPassword = $data['hashed_password'];
        /** @var string|null $dateNaissance */
        $dateNaissance = $data['date_naissance'];
        /** @var string $createdAt */
        $createdAt = $data['created_at'];
        /** @var string|null $activatedAt */
        $activatedAt = $data['activated_at'];
        /** @var string $firstName */
        $firstName = $data['first_name'] ?? '';
        /** @var string $lastName */
        $lastName = $data['last_name'] ?? '';
        /** @var string|null $invitedAt */
        $invitedAt = $data['invited_at'] ?? null;
        /** @var string|null $blockedAt */
        $blockedAt = $data['blocked_at'] ?? null;
        /** @var string|null $blockedReason */
        $blockedReason = $data['blocked_reason'] ?? null;
        /** @var string|null $imageRightsStatusValue */
        $imageRightsStatusValue = $data['image_rights_status'] ?? null;
        /** @var string|null $imageRightsUpdatedAt */
        $imageRightsUpdatedAt = $data['image_rights_updated_at'] ?? null;
        /** @var string|null $imageRightsUpdatedBy */
        $imageRightsUpdatedBy = $data['image_rights_updated_by'] ?? null;

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

        $consentement = null;
        /** @var array{parent_id: string, eleve_id: string, date_consentement: string, ip_address: string}|null $consentementData */
        $consentementData = $data['consentement_parental'] ?? null;
        if ($consentementData !== null) {
            $consentement = ConsentementParental::accorder(
                parentId: $consentementData['parent_id'],
                eleveId: $consentementData['eleve_id'],
                at: new DateTimeImmutable($consentementData['date_consentement']),
                ipAddress: $consentementData['ip_address'],
            );
        }

        return User::reconstitute(
            id: UserId::fromString($id),
            email: $email !== null ? new Email($email) : null,
            roles: $roles,
            tenantId: TenantId::fromString($tenantId),
            schoolName: $schoolName,
            statut: StatutCompte::from($statut),
            dateNaissance: $dateNaissance !== null ? new DateTimeImmutable($dateNaissance) : null,
            createdAt: new DateTimeImmutable($createdAt),
            hashedPassword: $hashedPassword,
            activatedAt: $activatedAt !== null ? new DateTimeImmutable($activatedAt) : null,
            consentementParental: $consentement,
            firstName: $firstName,
            lastName: $lastName,
            invitedAt: $invitedAt !== null ? new DateTimeImmutable($invitedAt) : null,
            blockedAt: $blockedAt !== null ? new DateTimeImmutable($blockedAt) : null,
            blockedReason: $blockedReason,
            imageRightsStatus: $imageRightsStatusValue !== null ? ImageRightsStatus::from($imageRightsStatusValue) : ImageRightsStatus::NOT_SPECIFIED,
            imageRightsUpdatedAt: $imageRightsUpdatedAt !== null ? new DateTimeImmutable($imageRightsUpdatedAt) : null,
            imageRightsUpdatedBy: $imageRightsUpdatedBy !== null ? UserId::fromString($imageRightsUpdatedBy) : null,
            studentNumber: isset($data['student_number']) && is_string($data['student_number']) ? $data['student_number'] : null,
        );
    }

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

    private function emailIndexKey(Email $email, TenantId $tenantId): string
    {
        return self::EMAIL_INDEX_PREFIX . $tenantId . ':' . $this->normalizeEmail($email);
    }
}
