<?php

declare(strict_types=1);

namespace App\Administration\Infrastructure\Persistence\Cache;

use App\Administration\Domain\Exception\StudentGuardianNotFoundException;
use App\Administration\Domain\Model\StudentGuardian\RelationshipType;
use App\Administration\Domain\Model\StudentGuardian\StudentGuardian;
use App\Administration\Domain\Model\StudentGuardian\StudentGuardianId;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Domain\Repository\StudentGuardianRepository;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use Override;
use Psr\Cache\CacheItemPoolInterface;

final readonly class CacheStudentGuardianRepository implements StudentGuardianRepository
{
    private const string KEY_BY_ID = 'sg:';
    private const string KEY_STUDENT = 'sg_student:';
    private const string KEY_GUARDIAN = 'sg_guardian:';
    private const string KEY_PAIR = 'sg_pair:';

    public function __construct(
        private StudentGuardianRepository $inner,
        private CacheItemPoolInterface $studentGuardiansCache,
    ) {
    }

    #[Override]
    public function save(StudentGuardian $link): void
    {
        $this->inner->save($link);
        $this->invalidateForLink($link);
    }

    #[Override]
    public function get(StudentGuardianId $id, TenantId $tenantId): StudentGuardian
    {
        $cacheKey = self::KEY_BY_ID . $id . ':' . $tenantId;
        $item = $this->studentGuardiansCache->getItem($cacheKey);

        if ($item->isHit()) {
            /** @var array{id: string, student_id: string, guardian_id: string, relationship_type: string, tenant_id: string, created_at: string, created_by: string|null} $data */
            $data = $item->get();

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

        $link = $this->inner->get($id, $tenantId);

        $item->set($this->serialize($link));
        $this->studentGuardiansCache->save($item);

        return $link;
    }

    #[Override]
    public function findGuardiansForStudent(UserId $studentId, TenantId $tenantId): array
    {
        $cacheKey = self::KEY_STUDENT . $studentId . ':' . $tenantId;
        $item = $this->studentGuardiansCache->getItem($cacheKey);

        if ($item->isHit()) {
            /** @var list<array{id: string, student_id: string, guardian_id: string, relationship_type: string, tenant_id: string, created_at: string, created_by: string|null}> $rows */
            $rows = $item->get();

            return array_map(fn (array $row) => $this->deserialize($row), $rows);
        }

        $links = $this->inner->findGuardiansForStudent($studentId, $tenantId);

        $item->set(array_map(fn (StudentGuardian $link) => $this->serialize($link), $links));
        $this->studentGuardiansCache->save($item);

        return $links;
    }

    #[Override]
    public function findStudentsForGuardian(UserId $guardianId, TenantId $tenantId): array
    {
        $cacheKey = self::KEY_GUARDIAN . $guardianId . ':' . $tenantId;
        $item = $this->studentGuardiansCache->getItem($cacheKey);

        if ($item->isHit()) {
            /** @var list<array{id: string, student_id: string, guardian_id: string, relationship_type: string, tenant_id: string, created_at: string, created_by: string|null}> $rows */
            $rows = $item->get();

            return array_map(fn (array $row) => $this->deserialize($row), $rows);
        }

        $links = $this->inner->findStudentsForGuardian($guardianId, $tenantId);

        $item->set(array_map(fn (StudentGuardian $link) => $this->serialize($link), $links));
        $this->studentGuardiansCache->save($item);

        return $links;
    }

    #[Override]
    public function countGuardiansForStudent(UserId $studentId, TenantId $tenantId): int
    {
        return $this->inner->countGuardiansForStudent($studentId, $tenantId);
    }

    #[Override]
    public function findByStudentAndGuardian(
        UserId $studentId,
        UserId $guardianId,
        TenantId $tenantId,
    ): ?StudentGuardian {
        $cacheKey = self::KEY_PAIR . $studentId . ':' . $guardianId . ':' . $tenantId;
        $item = $this->studentGuardiansCache->getItem($cacheKey);

        if ($item->isHit()) {
            /** @var array{id: string, student_id: string, guardian_id: string, relationship_type: string, tenant_id: string, created_at: string, created_by: string|null}|null $data */
            $data = $item->get();

            return $data !== null ? $this->deserialize($data) : null;
        }

        $link = $this->inner->findByStudentAndGuardian($studentId, $guardianId, $tenantId);

        $item->set($link !== null ? $this->serialize($link) : null);
        $this->studentGuardiansCache->save($item);

        return $link;
    }

    #[Override]
    public function delete(StudentGuardianId $id, TenantId $tenantId): void
    {
        try {
            $link = $this->get($id, $tenantId);
        } catch (StudentGuardianNotFoundException) {
            return;
        }

        $this->inner->delete($id, $tenantId);
        $this->invalidateForLink($link);
    }

    private function invalidateForLink(StudentGuardian $link): void
    {
        $this->studentGuardiansCache->deleteItem(self::KEY_BY_ID . $link->id . ':' . $link->tenantId);
        $this->studentGuardiansCache->deleteItem(self::KEY_STUDENT . $link->studentId . ':' . $link->tenantId);
        $this->studentGuardiansCache->deleteItem(self::KEY_GUARDIAN . $link->guardianId . ':' . $link->tenantId);
        $this->studentGuardiansCache->deleteItem(self::KEY_PAIR . $link->studentId . ':' . $link->guardianId . ':' . $link->tenantId);
    }

    /**
     * @return array{id: string, student_id: string, guardian_id: string, relationship_type: string, tenant_id: string, created_at: string, created_by: string|null}
     */
    private function serialize(StudentGuardian $link): array
    {
        return [
            'id' => (string) $link->id,
            'student_id' => (string) $link->studentId,
            'guardian_id' => (string) $link->guardianId,
            'relationship_type' => $link->relationshipType->value,
            'tenant_id' => (string) $link->tenantId,
            'created_at' => $link->createdAt->format(DateTimeImmutable::ATOM),
            'created_by' => $link->createdBy !== null ? (string) $link->createdBy : null,
        ];
    }

    /**
     * @param array{id: string, student_id: string, guardian_id: string, relationship_type: string, tenant_id: string, created_at: string, created_by: string|null} $data
     */
    private function deserialize(array $data): StudentGuardian
    {
        return StudentGuardian::reconstitute(
            id: StudentGuardianId::fromString($data['id']),
            studentId: UserId::fromString($data['student_id']),
            guardianId: UserId::fromString($data['guardian_id']),
            relationshipType: RelationshipType::from($data['relationship_type']),
            tenantId: TenantId::fromString($data['tenant_id']),
            createdAt: new DateTimeImmutable($data['created_at']),
            createdBy: $data['created_by'] !== null ? UserId::fromString($data['created_by']) : null,
        );
    }
}
