<?php

declare(strict_types=1);

namespace App\Scolarite\Infrastructure\Persistence\Doctrine;

use App\Administration\Domain\Model\SchoolClass\ClassId;
use App\Administration\Domain\Model\Subject\SubjectId;
use App\Administration\Domain\Model\User\UserId;
use App\Scolarite\Domain\Exception\EvaluationNotFoundException;
use App\Scolarite\Domain\Model\Evaluation\Coefficient;
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
use App\Scolarite\Domain\Model\Evaluation\EvaluationStatus;
use App\Scolarite\Domain\Model\Evaluation\GradeScale;
use App\Scolarite\Domain\Repository\EvaluationRepository;
use App\Shared\Domain\Tenant\TenantId;

use function array_map;

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

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

    #[Override]
    public function save(Evaluation $evaluation): void
    {
        $this->connection->executeStatement(
            'INSERT INTO evaluations (id, tenant_id, class_id, subject_id, teacher_id, title, description, evaluation_date, grade_scale, coefficient, status, created_at, updated_at, grades_published_at)
             VALUES (:id, :tenant_id, :class_id, :subject_id, :teacher_id, :title, :description, :evaluation_date, :grade_scale, :coefficient, :status, :created_at, :updated_at, :grades_published_at)
             ON CONFLICT (id) DO UPDATE SET
                 title = EXCLUDED.title,
                 description = EXCLUDED.description,
                 evaluation_date = EXCLUDED.evaluation_date,
                 grade_scale = EXCLUDED.grade_scale,
                 coefficient = EXCLUDED.coefficient,
                 status = EXCLUDED.status,
                 updated_at = EXCLUDED.updated_at,
                 grades_published_at = EXCLUDED.grades_published_at',
            [
                'id' => (string) $evaluation->id,
                'tenant_id' => (string) $evaluation->tenantId,
                'class_id' => (string) $evaluation->classId,
                'subject_id' => (string) $evaluation->subjectId,
                'teacher_id' => (string) $evaluation->teacherId,
                'title' => $evaluation->title,
                'description' => $evaluation->description,
                'evaluation_date' => $evaluation->evaluationDate->format('Y-m-d'),
                'grade_scale' => $evaluation->gradeScale->maxValue,
                'coefficient' => $evaluation->coefficient->value,
                'status' => $evaluation->status->value,
                'created_at' => $evaluation->createdAt->format(DateTimeImmutable::ATOM),
                'updated_at' => $evaluation->updatedAt->format(DateTimeImmutable::ATOM),
                'grades_published_at' => $evaluation->gradesPublishedAt?->format(DateTimeImmutable::ATOM),
            ],
        );
    }

    #[Override]
    public function get(EvaluationId $id, TenantId $tenantId): Evaluation
    {
        $evaluation = $this->findById($id, $tenantId);

        if ($evaluation === null) {
            throw EvaluationNotFoundException::withId($id);
        }

        return $evaluation;
    }

    #[Override]
    public function findById(EvaluationId $id, TenantId $tenantId): ?Evaluation
    {
        $row = $this->connection->fetchAssociative(
            'SELECT * FROM evaluations 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 findByTeacher(UserId $teacherId, TenantId $tenantId): array
    {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT e.*, c.name AS class_name, s.name AS subject_name
             FROM evaluations e
             LEFT JOIN school_classes c ON c.id = e.class_id
             LEFT JOIN subjects s ON s.id = e.subject_id
             WHERE e.teacher_id = :teacher_id
             AND e.tenant_id = :tenant_id
             AND e.status != :deleted
             ORDER BY e.evaluation_date DESC',
            [
                'teacher_id' => (string) $teacherId,
                'tenant_id' => (string) $tenantId,
                'deleted' => EvaluationStatus::DELETED->value,
            ],
        );

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

    #[Override]
    public function findByTeacherAndClass(UserId $teacherId, ClassId $classId, TenantId $tenantId): array
    {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT e.*, c.name AS class_name, s.name AS subject_name
             FROM evaluations e
             LEFT JOIN school_classes c ON c.id = e.class_id
             LEFT JOIN subjects s ON s.id = e.subject_id
             WHERE e.teacher_id = :teacher_id
             AND e.class_id = :class_id
             AND e.tenant_id = :tenant_id
             AND e.status != :deleted
             ORDER BY e.evaluation_date DESC',
            [
                'teacher_id' => (string) $teacherId,
                'class_id' => (string) $classId,
                'tenant_id' => (string) $tenantId,
                'deleted' => EvaluationStatus::DELETED->value,
            ],
        );

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

    #[Override]
    public function findByClass(ClassId $classId, TenantId $tenantId): array
    {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT e.*, c.name AS class_name, s.name AS subject_name
             FROM evaluations e
             LEFT JOIN school_classes c ON c.id = e.class_id
             LEFT JOIN subjects s ON s.id = e.subject_id
             WHERE e.class_id = :class_id
             AND e.tenant_id = :tenant_id
             AND e.status != :deleted
             ORDER BY e.evaluation_date DESC',
            [
                'class_id' => (string) $classId,
                'tenant_id' => (string) $tenantId,
                'deleted' => EvaluationStatus::DELETED->value,
            ],
        );

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

    #[Override]
    public function findAllWithPublishedGrades(TenantId $tenantId): array
    {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT * FROM evaluations
             WHERE tenant_id = :tenant_id
             AND status != :deleted
             AND grades_published_at IS NOT NULL
             ORDER BY evaluation_date ASC',
            [
                'tenant_id' => (string) $tenantId,
                'deleted' => EvaluationStatus::DELETED->value,
            ],
        );

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

    #[Override]
    public function findWithPublishedGradesBySubjectAndClassInDateRange(
        SubjectId $subjectId,
        ClassId $classId,
        DateTimeImmutable $startDate,
        DateTimeImmutable $endDate,
        TenantId $tenantId,
    ): array {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT * FROM evaluations
             WHERE subject_id = :subject_id
             AND class_id = :class_id
             AND tenant_id = :tenant_id
             AND status != :deleted
             AND grades_published_at IS NOT NULL
             AND evaluation_date BETWEEN :start_date AND :end_date
             ORDER BY evaluation_date ASC',
            [
                'subject_id' => (string) $subjectId,
                'class_id' => (string) $classId,
                'tenant_id' => (string) $tenantId,
                'deleted' => EvaluationStatus::DELETED->value,
                'start_date' => $startDate->format('Y-m-d'),
                'end_date' => $endDate->format('Y-m-d'),
            ],
        );

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

    /** @param array<string, mixed> $row */
    private function hydrate(array $row): Evaluation
    {
        /** @var string $id */
        $id = $row['id'];
        /** @var string $tenantId */
        $tenantId = $row['tenant_id'];
        /** @var string $classId */
        $classId = $row['class_id'];
        /** @var string $subjectId */
        $subjectId = $row['subject_id'];
        /** @var string $teacherId */
        $teacherId = $row['teacher_id'];
        /** @var string $title */
        $title = $row['title'];
        /** @var string|null $description */
        $description = $row['description'];
        /** @var string $evaluationDate */
        $evaluationDate = $row['evaluation_date'];
        /** @var string|int $gradeScaleRaw */
        $gradeScaleRaw = $row['grade_scale'];
        $gradeScale = (int) $gradeScaleRaw;
        /** @var string|float $coefficientRaw */
        $coefficientRaw = $row['coefficient'];
        $coefficient = (float) $coefficientRaw;
        /** @var string $status */
        $status = $row['status'];
        /** @var string $createdAt */
        $createdAt = $row['created_at'];
        /** @var string $updatedAt */
        $updatedAt = $row['updated_at'];
        /** @var string|null $gradesPublishedAt */
        $gradesPublishedAt = $row['grades_published_at'] ?? null;

        return Evaluation::reconstitute(
            id: EvaluationId::fromString($id),
            tenantId: TenantId::fromString($tenantId),
            classId: ClassId::fromString($classId),
            subjectId: SubjectId::fromString($subjectId),
            teacherId: UserId::fromString($teacherId),
            title: $title,
            description: $description,
            evaluationDate: new DateTimeImmutable($evaluationDate),
            gradeScale: new GradeScale($gradeScale),
            coefficient: new Coefficient($coefficient),
            status: EvaluationStatus::from($status),
            createdAt: new DateTimeImmutable($createdAt),
            updatedAt: new DateTimeImmutable($updatedAt),
            gradesPublishedAt: $gradesPublishedAt !== null ? new DateTimeImmutable($gradesPublishedAt) : null,
        );
    }
}
