<?php

declare(strict_types=1);

namespace App\Administration\Infrastructure\Persistence\Doctrine;

use App\Administration\Domain\Exception\ParentInvitationNotFoundException;
use App\Administration\Domain\Model\Invitation\InvitationCode;
use App\Administration\Domain\Model\Invitation\InvitationStatus;
use App\Administration\Domain\Model\Invitation\ParentInvitation;
use App\Administration\Domain\Model\Invitation\ParentInvitationId;
use App\Administration\Domain\Model\User\Email;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Domain\Repository\ParentInvitationRepository;
use App\Shared\Domain\Tenant\TenantId;

use function array_map;

use DateTimeImmutable;
use Doctrine\DBAL\Connection;
use Override;

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

    #[Override]
    public function save(ParentInvitation $invitation): void
    {
        $this->connection->executeStatement(
            <<<'SQL'
                INSERT INTO parent_invitations (
                    id, tenant_id, student_id, parent_email, code, status,
                    expires_at, created_at, created_by, sent_at,
                    activated_at, activated_user_id
                )
                VALUES (
                    :id, :tenant_id, :student_id, :parent_email, :code, :status,
                    :expires_at, :created_at, :created_by, :sent_at,
                    :activated_at, :activated_user_id
                )
                ON CONFLICT (id) DO UPDATE SET
                    code = EXCLUDED.code,
                    status = EXCLUDED.status,
                    expires_at = EXCLUDED.expires_at,
                    sent_at = EXCLUDED.sent_at,
                    activated_at = EXCLUDED.activated_at,
                    activated_user_id = EXCLUDED.activated_user_id
            SQL,
            [
                'id' => (string) $invitation->id,
                'tenant_id' => (string) $invitation->tenantId,
                'student_id' => (string) $invitation->studentId,
                'parent_email' => (string) $invitation->parentEmail,
                'code' => (string) $invitation->code,
                'status' => $invitation->status->value,
                'expires_at' => $invitation->expiresAt->format(DateTimeImmutable::ATOM),
                'created_at' => $invitation->createdAt->format(DateTimeImmutable::ATOM),
                'created_by' => (string) $invitation->createdBy,
                'sent_at' => $invitation->sentAt?->format(DateTimeImmutable::ATOM),
                'activated_at' => $invitation->activatedAt?->format(DateTimeImmutable::ATOM),
                'activated_user_id' => $invitation->activatedUserId !== null ? (string) $invitation->activatedUserId : null,
            ],
        );
    }

    #[Override]
    public function get(ParentInvitationId $id, TenantId $tenantId): ParentInvitation
    {
        $invitation = $this->findById($id, $tenantId);

        if ($invitation === null) {
            throw ParentInvitationNotFoundException::withId($id);
        }

        return $invitation;
    }

    #[Override]
    public function findById(ParentInvitationId $id, TenantId $tenantId): ?ParentInvitation
    {
        $row = $this->connection->fetchAssociative(
            'SELECT * FROM parent_invitations WHERE id = :id AND tenant_id = :tenant_id',
            [
                'id' => (string) $id,
                'tenant_id' => (string) $tenantId,
            ],
        );

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

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

    #[Override]
    public function findByCode(InvitationCode $code): ?ParentInvitation
    {
        $row = $this->connection->fetchAssociative(
            'SELECT * FROM parent_invitations WHERE code = :code',
            ['code' => (string) $code],
        );

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

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

    #[Override]
    public function findAllByTenant(TenantId $tenantId): array
    {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT * FROM parent_invitations WHERE tenant_id = :tenant_id ORDER BY created_at DESC',
            ['tenant_id' => (string) $tenantId],
        );

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

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

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

    #[Override]
    public function findByStatus(InvitationStatus $status, TenantId $tenantId): array
    {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT * FROM parent_invitations WHERE status = :status AND tenant_id = :tenant_id ORDER BY created_at DESC',
            [
                'status' => $status->value,
                'tenant_id' => (string) $tenantId,
            ],
        );

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

    #[Override]
    public function findExpiredSent(DateTimeImmutable $at): array
    {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT * FROM parent_invitations WHERE status = :status AND expires_at <= :at',
            [
                'status' => InvitationStatus::SENT->value,
                'at' => $at->format(DateTimeImmutable::ATOM),
            ],
        );

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

    #[Override]
    public function delete(ParentInvitationId $id, TenantId $tenantId): void
    {
        $this->connection->executeStatement(
            'DELETE FROM parent_invitations WHERE id = :id AND tenant_id = :tenant_id',
            [
                'id' => (string) $id,
                'tenant_id' => (string) $tenantId,
            ],
        );
    }

    /**
     * @param array<string, mixed> $row
     */
    private function hydrate(array $row): ParentInvitation
    {
        /** @var string $id */
        $id = $row['id'];
        /** @var string $tenantId */
        $tenantId = $row['tenant_id'];
        /** @var string $studentId */
        $studentId = $row['student_id'];
        /** @var string $parentEmail */
        $parentEmail = $row['parent_email'];
        /** @var string $code */
        $code = $row['code'];
        /** @var string $status */
        $status = $row['status'];
        /** @var string $expiresAt */
        $expiresAt = $row['expires_at'];
        /** @var string $createdAt */
        $createdAt = $row['created_at'];
        /** @var string $createdBy */
        $createdBy = $row['created_by'];
        /** @var string|null $sentAt */
        $sentAt = $row['sent_at'];
        /** @var string|null $activatedAt */
        $activatedAt = $row['activated_at'];
        /** @var string|null $activatedUserId */
        $activatedUserId = $row['activated_user_id'];

        return ParentInvitation::reconstitute(
            id: ParentInvitationId::fromString($id),
            tenantId: TenantId::fromString($tenantId),
            studentId: UserId::fromString($studentId),
            parentEmail: new Email($parentEmail),
            code: new InvitationCode($code),
            status: InvitationStatus::from($status),
            expiresAt: new DateTimeImmutable($expiresAt),
            createdAt: new DateTimeImmutable($createdAt),
            createdBy: UserId::fromString($createdBy),
            sentAt: $sentAt !== null ? new DateTimeImmutable($sentAt) : null,
            activatedAt: $activatedAt !== null ? new DateTimeImmutable($activatedAt) : null,
            activatedUserId: $activatedUserId !== null ? UserId::fromString($activatedUserId) : null,
        );
    }
}
