<?php

declare(strict_types=1);

namespace App\Administration\Infrastructure\Persistence\Doctrine;

use App\Administration\Domain\Exception\LiaisonDejaExistanteException;
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 Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Override;

final readonly class DoctrineStudentGuardianRepository implements StudentGuardianRepository
{
    public function __construct(
        private Connection $connection,
    ) {
    }

    #[Override]
    public function save(StudentGuardian $link): void
    {
        try {
            $this->connection->executeStatement(
                'INSERT INTO student_guardians (id, student_id, guardian_id, relationship_type, tenant_id, created_at, created_by)
                 VALUES (:id, :student_id, :guardian_id, :relationship_type, :tenant_id, :created_at, :created_by)
                 ON CONFLICT (student_id, guardian_id, tenant_id) DO UPDATE SET
                     relationship_type = EXCLUDED.relationship_type',
                [
                    '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,
                ],
            );
        } catch (UniqueConstraintViolationException) {
            throw LiaisonDejaExistanteException::pourParentEtEleve($link->guardianId, $link->studentId);
        }
    }

    #[Override]
    public function get(StudentGuardianId $id, TenantId $tenantId): StudentGuardian
    {
        $link = $this->findById($id, $tenantId);

        if ($link === null) {
            throw StudentGuardianNotFoundException::withId($id);
        }

        return $link;
    }

    #[Override]
    public function findGuardiansForStudent(UserId $studentId, TenantId $tenantId): array
    {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT * FROM student_guardians
             WHERE student_id = :student_id
             AND tenant_id = :tenant_id
             ORDER BY created_at ASC',
            [
                'student_id' => (string) $studentId,
                'tenant_id' => (string) $tenantId,
            ],
        );

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

    #[Override]
    public function findStudentsForGuardian(UserId $guardianId, TenantId $tenantId): array
    {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT * FROM student_guardians
             WHERE guardian_id = :guardian_id
             AND tenant_id = :tenant_id
             ORDER BY created_at ASC',
            [
                'guardian_id' => (string) $guardianId,
                'tenant_id' => (string) $tenantId,
            ],
        );

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

    #[Override]
    public function countGuardiansForStudent(UserId $studentId, TenantId $tenantId): int
    {
        /** @var int|string $count */
        $count = $this->connection->fetchOne(
            'SELECT COUNT(*) FROM student_guardians
             WHERE student_id = :student_id
             AND tenant_id = :tenant_id',
            [
                'student_id' => (string) $studentId,
                'tenant_id' => (string) $tenantId,
            ],
        );

        return (int) $count;
    }

    #[Override]
    public function findByStudentAndGuardian(
        UserId $studentId,
        UserId $guardianId,
        TenantId $tenantId,
    ): ?StudentGuardian {
        $row = $this->connection->fetchAssociative(
            'SELECT * FROM student_guardians
             WHERE student_id = :student_id
             AND guardian_id = :guardian_id
             AND tenant_id = :tenant_id',
            [
                'student_id' => (string) $studentId,
                'guardian_id' => (string) $guardianId,
                'tenant_id' => (string) $tenantId,
            ],
        );

        if ($row === false) {
            return null;
        }

        return $this->hydrate($row);
    }

    #[Override]
    public function delete(StudentGuardianId $id, TenantId $tenantId): void
    {
        $this->connection->delete('student_guardians', [
            'id' => (string) $id,
            'tenant_id' => (string) $tenantId,
        ]);
    }

    private function findById(StudentGuardianId $id, TenantId $tenantId): ?StudentGuardian
    {
        $row = $this->connection->fetchAssociative(
            'SELECT * FROM student_guardians WHERE id = :id AND tenant_id = :tenant_id',
            [
                'id' => (string) $id,
                'tenant_id' => (string) $tenantId,
            ],
        );

        if ($row === false) {
            return null;
        }

        return $this->hydrate($row);
    }

    /**
     * @param array<string, mixed> $row
     */
    private function hydrate(array $row): StudentGuardian
    {
        /** @var string $id */
        $id = $row['id'];
        /** @var string $studentId */
        $studentId = $row['student_id'];
        /** @var string $guardianId */
        $guardianId = $row['guardian_id'];
        /** @var string $relationshipType */
        $relationshipType = $row['relationship_type'];
        /** @var string $tenantId */
        $tenantId = $row['tenant_id'];
        /** @var string $createdAt */
        $createdAt = $row['created_at'];
        /** @var string|null $createdBy */
        $createdBy = $row['created_by'];

        return StudentGuardian::reconstitute(
            id: StudentGuardianId::fromString($id),
            studentId: UserId::fromString($studentId),
            guardianId: UserId::fromString($guardianId),
            relationshipType: RelationshipType::from($relationshipType),
            tenantId: TenantId::fromString($tenantId),
            createdAt: new DateTimeImmutable($createdAt),
            createdBy: $createdBy !== null ? UserId::fromString($createdBy) : null,
        );
    }
}
