<?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\ScheduleSlotNotFoundException;
use App\Scolarite\Domain\Model\Schedule\DayOfWeek;
use App\Scolarite\Domain\Model\Schedule\ScheduleSlot;
use App\Scolarite\Domain\Model\Schedule\ScheduleSlotId;
use App\Scolarite\Domain\Model\Schedule\TimeSlot;
use App\Scolarite\Domain\Repository\ScheduleSlotRepository;
use App\Shared\Domain\Tenant\TenantId;

use function array_map;

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

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

    #[Override]
    public function save(ScheduleSlot $slot): void
    {
        $this->connection->executeStatement(
            'INSERT INTO schedule_slots (id, tenant_id, class_id, subject_id, teacher_id, day_of_week, start_time, end_time, room, is_recurring, recurrence_start, recurrence_end, created_at, updated_at)
             VALUES (:id, :tenant_id, :class_id, :subject_id, :teacher_id, :day_of_week, :start_time, :end_time, :room, :is_recurring, :recurrence_start, :recurrence_end, :created_at, :updated_at)
             ON CONFLICT (id) DO UPDATE SET
                 class_id = EXCLUDED.class_id,
                 subject_id = EXCLUDED.subject_id,
                 teacher_id = EXCLUDED.teacher_id,
                 day_of_week = EXCLUDED.day_of_week,
                 start_time = EXCLUDED.start_time,
                 end_time = EXCLUDED.end_time,
                 room = EXCLUDED.room,
                 recurrence_start = EXCLUDED.recurrence_start,
                 recurrence_end = EXCLUDED.recurrence_end,
                 updated_at = EXCLUDED.updated_at',
            [
                'id' => (string) $slot->id,
                'tenant_id' => (string) $slot->tenantId,
                'class_id' => (string) $slot->classId,
                'subject_id' => (string) $slot->subjectId,
                'teacher_id' => (string) $slot->teacherId,
                'day_of_week' => $slot->dayOfWeek->value,
                'start_time' => $slot->timeSlot->startTime,
                'end_time' => $slot->timeSlot->endTime,
                'room' => $slot->room,
                'is_recurring' => $slot->isRecurring ? 'true' : 'false',
                'recurrence_start' => $slot->recurrenceStart?->format('Y-m-d'),
                'recurrence_end' => $slot->recurrenceEnd?->format('Y-m-d'),
                'created_at' => $slot->createdAt->format(DateTimeImmutable::ATOM),
                'updated_at' => $slot->updatedAt->format(DateTimeImmutable::ATOM),
            ],
        );
    }

    #[Override]
    public function get(ScheduleSlotId $id, TenantId $tenantId): ScheduleSlot
    {
        $slot = $this->findById($id, $tenantId);

        if ($slot === null) {
            throw ScheduleSlotNotFoundException::avecId($id);
        }

        return $slot;
    }

    #[Override]
    public function findById(ScheduleSlotId $id, TenantId $tenantId): ?ScheduleSlot
    {
        $row = $this->connection->fetchAssociative(
            'SELECT * FROM schedule_slots 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 delete(ScheduleSlotId $id, TenantId $tenantId): void
    {
        $this->connection->executeStatement(
            'DELETE FROM schedule_slots WHERE id = :id AND tenant_id = :tenant_id',
            ['id' => (string) $id, 'tenant_id' => (string) $tenantId],
        );
    }

    #[Override]
    public function findByClass(ClassId $classId, TenantId $tenantId): array
    {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT * FROM schedule_slots
             WHERE class_id = :class_id AND tenant_id = :tenant_id
             ORDER BY day_of_week, start_time',
            ['class_id' => (string) $classId, 'tenant_id' => (string) $tenantId],
        );

        return $this->hydrateMany($rows);
    }

    #[Override]
    public function findRecurringByClass(ClassId $classId, TenantId $tenantId): array
    {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT * FROM schedule_slots
             WHERE class_id = :class_id AND tenant_id = :tenant_id AND is_recurring = true
             ORDER BY day_of_week, start_time',
            ['class_id' => (string) $classId, 'tenant_id' => (string) $tenantId],
        );

        return $this->hydrateMany($rows);
    }

    #[Override]
    public function findByTeacher(UserId $teacherId, TenantId $tenantId): array
    {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT * FROM schedule_slots
             WHERE teacher_id = :teacher_id AND tenant_id = :tenant_id
             ORDER BY day_of_week, start_time',
            ['teacher_id' => (string) $teacherId, 'tenant_id' => (string) $tenantId],
        );

        return $this->hydrateMany($rows);
    }

    #[Override]
    public function findOverlappingForClass(
        ClassId $classId,
        DayOfWeek $dayOfWeek,
        string $startTime,
        string $endTime,
        TenantId $tenantId,
        ?ScheduleSlotId $excludeId = null,
    ): array {
        $sql = 'SELECT * FROM schedule_slots
                WHERE tenant_id = :tenant_id
                AND class_id = :class_id
                AND day_of_week = :day_of_week
                AND start_time < :end_time
                AND end_time > :start_time';
        $params = [
            'tenant_id' => (string) $tenantId,
            'class_id' => (string) $classId,
            'day_of_week' => $dayOfWeek->value,
            'start_time' => $startTime,
            'end_time' => $endTime,
        ];

        if ($excludeId !== null) {
            $sql .= ' AND id != :exclude_id';
            $params['exclude_id'] = (string) $excludeId;
        }

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

        return $this->hydrateMany($rows);
    }

    #[Override]
    public function findOverlappingForTeacher(
        UserId $teacherId,
        DayOfWeek $dayOfWeek,
        string $startTime,
        string $endTime,
        TenantId $tenantId,
        ?ScheduleSlotId $excludeId = null,
    ): array {
        $sql = 'SELECT * FROM schedule_slots
                WHERE tenant_id = :tenant_id
                AND teacher_id = :teacher_id
                AND day_of_week = :day_of_week
                AND start_time < :end_time
                AND end_time > :start_time';
        $params = [
            'tenant_id' => (string) $tenantId,
            'teacher_id' => (string) $teacherId,
            'day_of_week' => $dayOfWeek->value,
            'start_time' => $startTime,
            'end_time' => $endTime,
        ];

        if ($excludeId !== null) {
            $sql .= ' AND id != :exclude_id';
            $params['exclude_id'] = (string) $excludeId;
        }

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

        return $this->hydrateMany($rows);
    }

    #[Override]
    public function findOverlappingForRoom(
        string $room,
        DayOfWeek $dayOfWeek,
        string $startTime,
        string $endTime,
        TenantId $tenantId,
        ?ScheduleSlotId $excludeId = null,
    ): array {
        $sql = 'SELECT * FROM schedule_slots
                WHERE tenant_id = :tenant_id
                AND room = :room
                AND day_of_week = :day_of_week
                AND start_time < :end_time
                AND end_time > :start_time';
        $params = [
            'tenant_id' => (string) $tenantId,
            'room' => $room,
            'day_of_week' => $dayOfWeek->value,
            'start_time' => $startTime,
            'end_time' => $endTime,
        ];

        if ($excludeId !== null) {
            $sql .= ' AND id != :exclude_id';
            $params['exclude_id'] = (string) $excludeId;
        }

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

        return $this->hydrateMany($rows);
    }

    /**
     * @param list<array<string, mixed>> $rows
     *
     * @return list<ScheduleSlot>
     */
    private function hydrateMany(array $rows): array
    {
        return array_map($this->hydrate(...), $rows);
    }

    /**
     * @param array<string, mixed> $row
     */
    private function hydrate(array $row): ScheduleSlot
    {
        /** @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 int $dayOfWeek */
        $dayOfWeek = $row['day_of_week'];
        /** @var string $startTime */
        $startTime = $row['start_time'];
        /** @var string $endTime */
        $endTime = $row['end_time'];
        /** @var string|null $room */
        $room = $row['room'];
        /** @var bool $isRecurring */
        $isRecurring = $row['is_recurring'];
        /** @var string|null $recurrenceStart */
        $recurrenceStart = $row['recurrence_start'] ?? null;
        /** @var string|null $recurrenceEnd */
        $recurrenceEnd = $row['recurrence_end'] ?? null;
        /** @var string $createdAt */
        $createdAt = $row['created_at'];
        /** @var string $updatedAt */
        $updatedAt = $row['updated_at'];

        return ScheduleSlot::reconstitute(
            id: ScheduleSlotId::fromString($id),
            tenantId: TenantId::fromString($tenantId),
            classId: ClassId::fromString($classId),
            subjectId: SubjectId::fromString($subjectId),
            teacherId: UserId::fromString($teacherId),
            dayOfWeek: DayOfWeek::from((int) $dayOfWeek),
            timeSlot: new TimeSlot($startTime, $endTime),
            room: $room,
            isRecurring: (bool) $isRecurring,
            createdAt: new DateTimeImmutable($createdAt),
            updatedAt: new DateTimeImmutable($updatedAt),
            recurrenceStart: $recurrenceStart !== null ? new DateTimeImmutable($recurrenceStart) : null,
            recurrenceEnd: $recurrenceEnd !== null ? new DateTimeImmutable($recurrenceEnd) : null,
        );
    }
}
