<?php

declare(strict_types=1);

namespace App\Administration\Infrastructure\Persistence\Doctrine;

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 Doctrine\DBAL\Connection;

use function is_array;

use Override;
use RuntimeException;

use function sprintf;

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

    #[Override]
    public function save(User $user): void
    {
        $consentement = $user->consentementParental;

        $this->connection->executeStatement(
            <<<'SQL'
                INSERT INTO users (
                    id, tenant_id, email, first_name, last_name, roles,
                    hashed_password, statut, school_name, date_naissance,
                    created_at, activated_at, invited_at, blocked_at, blocked_reason,
                    consentement_parent_id, consentement_eleve_id, consentement_date, consentement_ip,
                    updated_at
                )
                VALUES (
                    :id, :tenant_id, :email, :first_name, :last_name, :roles,
                    :hashed_password, :statut, :school_name, :date_naissance,
                    :created_at, :activated_at, :invited_at, :blocked_at, :blocked_reason,
                    :consentement_parent_id, :consentement_eleve_id, :consentement_date, :consentement_ip,
                    NOW()
                )
                ON CONFLICT (id) DO UPDATE SET
                    email = EXCLUDED.email,
                    first_name = EXCLUDED.first_name,
                    last_name = EXCLUDED.last_name,
                    roles = EXCLUDED.roles,
                    hashed_password = EXCLUDED.hashed_password,
                    statut = EXCLUDED.statut,
                    school_name = EXCLUDED.school_name,
                    date_naissance = EXCLUDED.date_naissance,
                    activated_at = EXCLUDED.activated_at,
                    invited_at = EXCLUDED.invited_at,
                    blocked_at = EXCLUDED.blocked_at,
                    blocked_reason = EXCLUDED.blocked_reason,
                    consentement_parent_id = EXCLUDED.consentement_parent_id,
                    consentement_eleve_id = EXCLUDED.consentement_eleve_id,
                    consentement_date = EXCLUDED.consentement_date,
                    consentement_ip = EXCLUDED.consentement_ip,
                    updated_at = NOW()
            SQL,
            [
                'id' => (string) $user->id,
                'tenant_id' => (string) $user->tenantId,
                'email' => (string) $user->email,
                'first_name' => $user->firstName,
                'last_name' => $user->lastName,
                'roles' => json_encode(array_map(static fn (Role $r) => $r->value, $user->roles)),
                'hashed_password' => $user->hashedPassword,
                'statut' => $user->statut->value,
                'school_name' => $user->schoolName,
                'date_naissance' => $user->dateNaissance?->format('Y-m-d'),
                'created_at' => $user->createdAt->format(DateTimeImmutable::ATOM),
                'activated_at' => $user->activatedAt?->format(DateTimeImmutable::ATOM),
                'invited_at' => $user->invitedAt?->format(DateTimeImmutable::ATOM),
                'blocked_at' => $user->blockedAt?->format(DateTimeImmutable::ATOM),
                'blocked_reason' => $user->blockedReason,
                'consentement_parent_id' => $consentement?->parentId,
                'consentement_eleve_id' => $consentement?->eleveId,
                'consentement_date' => $consentement?->dateConsentement->format(DateTimeImmutable::ATOM),
                'consentement_ip' => $consentement?->ipAddress,
            ],
        );
    }

    #[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
    {
        $row = $this->connection->fetchAssociative(
            'SELECT * FROM users WHERE id = :id',
            ['id' => (string) $id],
        );

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

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

    #[Override]
    public function findByEmail(Email $email, TenantId $tenantId): ?User
    {
        $row = $this->connection->fetchAssociative(
            'SELECT * FROM users WHERE tenant_id = :tenant_id AND email = :email',
            [
                'tenant_id' => (string) $tenantId,
                'email' => (string) $email,
            ],
        );

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

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

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

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

    /**
     * @param array<string, mixed> $row
     */
    private function hydrate(array $row): User
    {
        /** @var string $id */
        $id = $row['id'];
        /** @var string $tenantId */
        $tenantId = $row['tenant_id'];
        /** @var string $email */
        $email = $row['email'];
        /** @var string $firstName */
        $firstName = $row['first_name'];
        /** @var string $lastName */
        $lastName = $row['last_name'];
        /** @var string $rolesJson */
        $rolesJson = $row['roles'];
        /** @var string|null $hashedPassword */
        $hashedPassword = $row['hashed_password'];
        /** @var string $statut */
        $statut = $row['statut'];
        /** @var string $schoolName */
        $schoolName = $row['school_name'];
        /** @var string|null $dateNaissance */
        $dateNaissance = $row['date_naissance'];
        /** @var string $createdAt */
        $createdAt = $row['created_at'];
        /** @var string|null $activatedAt */
        $activatedAt = $row['activated_at'];
        /** @var string|null $invitedAt */
        $invitedAt = $row['invited_at'];
        /** @var string|null $blockedAt */
        $blockedAt = $row['blocked_at'];
        /** @var string|null $blockedReason */
        $blockedReason = $row['blocked_reason'];
        /** @var string|null $consentementParentId */
        $consentementParentId = $row['consentement_parent_id'];
        /** @var string|null $consentementEleveId */
        $consentementEleveId = $row['consentement_eleve_id'];
        /** @var string|null $consentementDate */
        $consentementDate = $row['consentement_date'];
        /** @var string|null $consentementIp */
        $consentementIp = $row['consentement_ip'];

        /** @var string[]|null $roleValues */
        $roleValues = json_decode($rolesJson, true);

        if (!is_array($roleValues)) {
            throw new RuntimeException(sprintf('Invalid roles JSON for user %s: %s', $id, $rolesJson));
        }

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

        $consentement = null;
        if ($consentementParentId !== null && $consentementEleveId !== null && $consentementDate !== null) {
            $consentement = ConsentementParental::accorder(
                parentId: $consentementParentId,
                eleveId: $consentementEleveId,
                at: new DateTimeImmutable($consentementDate),
                ipAddress: $consentementIp ?? '',
            );
        }

        return User::reconstitute(
            id: UserId::fromString($id),
            email: new Email($email),
            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,
        );
    }
}
