<?php

declare(strict_types=1);

namespace App\Administration\Infrastructure\ReadModel;

use App\Administration\Application\Dto\PaginatedResult;
use App\Administration\Application\Port\PaginatedUsersReader;
use App\Administration\Application\Query\GetUsers\UserDto;
use App\Administration\Domain\Model\User\Role;
use App\Shared\Domain\Clock;
use DateTimeImmutable;
use Doctrine\DBAL\Connection;

use function json_decode;
use function json_encode;

use const JSON_THROW_ON_ERROR;

final readonly class DbalPaginatedUsersReader implements PaginatedUsersReader
{
    private const int INVITATION_EXPIRY_DAYS = 7;

    public function __construct(
        private Connection $connection,
        private Clock $clock,
    ) {
    }

    /**
     * @return PaginatedResult<UserDto>
     */
    public function findPaginated(
        string $tenantId,
        ?string $role,
        ?string $statut,
        ?string $search,
        int $page,
        int $limit,
    ): PaginatedResult {
        $params = ['tenant_id' => $tenantId];
        $whereClause = 'u.tenant_id = :tenant_id';

        if ($role !== null) {
            $filterRole = Role::tryFrom($role);
            if ($filterRole !== null) {
                $whereClause .= ' AND u.roles::jsonb @> :role::jsonb';
                $params['role'] = json_encode([$filterRole->value], JSON_THROW_ON_ERROR);
            }
        }

        if ($statut !== null) {
            $whereClause .= ' AND u.statut = :statut';
            $params['statut'] = $statut;
        }

        if ($search !== null && $search !== '') {
            $whereClause .= ' AND (u.first_name ILIKE :search OR u.last_name ILIKE :search OR u.email ILIKE :search)';
            $params['search'] = '%' . $search . '%';
        }

        $countSql = "SELECT COUNT(*) FROM users u WHERE {$whereClause}";

        /** @var int|string|false $totalRaw */
        $totalRaw = $this->connection->fetchOne($countSql, $params);
        $total = (int) $totalRaw;

        $offset = ($page - 1) * $limit;

        $selectSql = <<<SQL
            SELECT
                u.id, u.email, u.roles, u.first_name, u.last_name,
                u.statut, u.created_at, u.invited_at, u.activated_at,
                u.blocked_at, u.blocked_reason
            FROM users u
            WHERE {$whereClause}
            ORDER BY u.last_name ASC, u.first_name ASC
            LIMIT :limit OFFSET :offset
        SQL;

        $params['limit'] = $limit;
        $params['offset'] = $offset;

        $rows = $this->connection->fetchAllAssociative($selectSql, $params);

        $now = $this->clock->now();
        $items = array_map(
            static fn (array $row): UserDto => self::mapRowToDto($row, $now),
            $rows,
        );

        return new PaginatedResult(
            items: $items,
            total: $total,
            page: $page,
            limit: $limit,
        );
    }

    /**
     * @param array<string, mixed> $row
     */
    private static function mapRowToDto(array $row, DateTimeImmutable $now): UserDto
    {
        /** @var string $id */
        $id = $row['id'];
        /** @var string|null $email */
        $email = $row['email'];
        /** @var string $rolesJson */
        $rolesJson = $row['roles'];
        /** @var string $firstName */
        $firstName = $row['first_name'];
        /** @var string $lastName */
        $lastName = $row['last_name'];
        /** @var string $statut */
        $statut = $row['statut'];
        /** @var string $createdAt */
        $createdAt = $row['created_at'];
        /** @var string|null $invitedAt */
        $invitedAt = $row['invited_at'];
        /** @var string|null $activatedAt */
        $activatedAt = $row['activated_at'];
        /** @var string|null $blockedAt */
        $blockedAt = $row['blocked_at'];
        /** @var string|null $blockedReason */
        $blockedReason = $row['blocked_reason'];

        /** @var string[] $roleValues */
        $roleValues = json_decode($rolesJson, true, 512, JSON_THROW_ON_ERROR);
        $primaryRole = Role::from($roleValues[0] ?? Role::ELEVE->value);

        $invitedAtDate = $invitedAt !== null ? new DateTimeImmutable($invitedAt) : null;
        $invitationExpiree = $invitedAtDate !== null
            && $activatedAt === null
            && $invitedAtDate->modify('+' . self::INVITATION_EXPIRY_DAYS . ' days') < $now;

        return new UserDto(
            id: $id,
            email: $email ?? '',
            role: $primaryRole->value,
            roleLabel: $primaryRole->label(),
            roles: $roleValues,
            firstName: $firstName,
            lastName: $lastName,
            statut: $statut,
            createdAt: new DateTimeImmutable($createdAt),
            invitedAt: $invitedAtDate,
            activatedAt: $activatedAt !== null ? new DateTimeImmutable($activatedAt) : null,
            blockedAt: $blockedAt !== null ? new DateTimeImmutable($blockedAt) : null,
            blockedReason: $blockedReason,
            invitationExpiree: $invitationExpiree,
        );
    }
}
