<?php

declare(strict_types=1);

namespace App\Scolarite\Infrastructure\Persistence\Doctrine;

use App\Administration\Domain\Model\User\UserId;
use App\Scolarite\Domain\Exception\GradeNotFoundException;
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
use App\Scolarite\Domain\Model\Grade\Grade;
use App\Scolarite\Domain\Model\Grade\GradeId;
use App\Scolarite\Domain\Model\Grade\GradeStatus;
use App\Scolarite\Domain\Model\Grade\GradeValue;
use App\Scolarite\Domain\Repository\GradeRepository;
use App\Shared\Domain\Tenant\TenantId;

use function array_map;

use DateTimeImmutable;
use Doctrine\DBAL\Connection;

use function implode;

use Override;

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

    #[Override]
    public function save(Grade $grade): void
    {
        $this->connection->executeStatement(
            'INSERT INTO grades (id, tenant_id, evaluation_id, student_id, value, status, created_by, created_at, updated_at, appreciation, appreciation_updated_at)
             VALUES (:id, :tenant_id, :evaluation_id, :student_id, :value, :status, :created_by, :created_at, :updated_at, :appreciation, :appreciation_updated_at)
             ON CONFLICT (evaluation_id, student_id) DO UPDATE SET
                 value = EXCLUDED.value,
                 status = EXCLUDED.status,
                 updated_at = EXCLUDED.updated_at',
            [
                'id' => (string) $grade->id,
                'tenant_id' => (string) $grade->tenantId,
                'evaluation_id' => (string) $grade->evaluationId,
                'student_id' => (string) $grade->studentId,
                'value' => $grade->value?->value,
                'status' => $grade->status->value,
                'created_by' => (string) $grade->createdBy,
                'created_at' => $grade->createdAt->format(DateTimeImmutable::ATOM),
                'updated_at' => $grade->updatedAt->format(DateTimeImmutable::ATOM),
                'appreciation' => $grade->appreciation,
                'appreciation_updated_at' => $grade->appreciationUpdatedAt?->format(DateTimeImmutable::ATOM),
            ],
        );
    }

    #[Override]
    public function saveAppreciation(Grade $grade): void
    {
        $this->connection->executeStatement(
            'UPDATE grades SET appreciation = :appreciation, appreciation_updated_at = :appreciation_updated_at, updated_at = :updated_at
             WHERE id = :id AND tenant_id = :tenant_id',
            [
                'id' => (string) $grade->id,
                'tenant_id' => (string) $grade->tenantId,
                'appreciation' => $grade->appreciation,
                'appreciation_updated_at' => $grade->appreciationUpdatedAt?->format(DateTimeImmutable::ATOM),
                'updated_at' => $grade->updatedAt->format(DateTimeImmutable::ATOM),
            ],
        );
    }

    #[Override]
    public function get(GradeId $id, TenantId $tenantId): Grade
    {
        $grade = $this->findById($id, $tenantId);

        if ($grade === null) {
            throw GradeNotFoundException::withId($id);
        }

        return $grade;
    }

    #[Override]
    public function findById(GradeId $id, TenantId $tenantId): ?Grade
    {
        $row = $this->connection->fetchAssociative(
            'SELECT * FROM grades 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 findByEvaluation(EvaluationId $evaluationId, TenantId $tenantId): array
    {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT * FROM grades
             WHERE evaluation_id = :evaluation_id
             AND tenant_id = :tenant_id
             ORDER BY created_at ASC',
            [
                'evaluation_id' => (string) $evaluationId,
                'tenant_id' => (string) $tenantId,
            ],
        );

        return array_map($this->hydrate(...), $rows);
    }

    #[Override]
    public function findByEvaluations(array $evaluationIds, TenantId $tenantId): array
    {
        if ($evaluationIds === []) {
            return [];
        }

        $placeholders = [];
        $params = ['tenant_id' => (string) $tenantId];

        foreach ($evaluationIds as $i => $evalId) {
            $key = 'eval_' . $i;
            $placeholders[] = ':' . $key;
            $params[$key] = (string) $evalId;
        }

        $in = implode(', ', $placeholders);

        $rows = $this->connection->fetchAllAssociative(
            "SELECT * FROM grades
             WHERE evaluation_id IN ({$in})
             AND tenant_id = :tenant_id
             ORDER BY created_at ASC",
            $params,
        );

        $grouped = [];

        foreach ($rows as $row) {
            /** @var string $evalId */
            $evalId = $row['evaluation_id'];
            $grouped[$evalId][] = $this->hydrate($row);
        }

        return $grouped;
    }

    #[Override]
    public function hasGradesForEvaluation(EvaluationId $evaluationId, TenantId $tenantId): bool
    {
        $count = $this->connection->fetchOne(
            'SELECT COUNT(*) FROM grades WHERE evaluation_id = :evaluation_id AND tenant_id = :tenant_id',
            [
                'evaluation_id' => (string) $evaluationId,
                'tenant_id' => (string) $tenantId,
            ],
        );

        /** @var int|string|false $countValue */
        $countValue = $count;

        return (int) $countValue > 0;
    }

    /** @param array<string, mixed> $row */
    private function hydrate(array $row): Grade
    {
        /** @var string $id */
        $id = $row['id'];
        /** @var string $tenantId */
        $tenantId = $row['tenant_id'];
        /** @var string $evaluationId */
        $evaluationId = $row['evaluation_id'];
        /** @var string $studentId */
        $studentId = $row['student_id'];
        /** @var string|float|null $valueRaw */
        $valueRaw = $row['value'];
        /** @var string $status */
        $status = $row['status'];
        /** @var string $createdBy */
        $createdBy = $row['created_by'];
        /** @var string $createdAt */
        $createdAt = $row['created_at'];
        /** @var string $updatedAt */
        $updatedAt = $row['updated_at'];
        /** @var string|null $appreciation */
        $appreciation = $row['appreciation'] ?? null;
        /** @var string|null $appreciationUpdatedAt */
        $appreciationUpdatedAt = $row['appreciation_updated_at'] ?? null;

        return Grade::reconstitute(
            id: GradeId::fromString($id),
            tenantId: TenantId::fromString($tenantId),
            evaluationId: EvaluationId::fromString($evaluationId),
            studentId: UserId::fromString($studentId),
            value: $valueRaw !== null ? new GradeValue((float) $valueRaw) : null,
            status: GradeStatus::from($status),
            createdBy: UserId::fromString($createdBy),
            createdAt: new DateTimeImmutable($createdAt),
            updatedAt: new DateTimeImmutable($updatedAt),
            appreciation: $appreciation,
            appreciationUpdatedAt: $appreciationUpdatedAt !== null ? new DateTimeImmutable($appreciationUpdatedAt) : null,
        );
    }
}
