<?php

declare(strict_types=1);

namespace App\Scolarite\Infrastructure\Persistence\Doctrine;

use App\Administration\Domain\Model\User\UserId;
use App\Scolarite\Domain\Model\Schedule\ScheduleException;
use App\Scolarite\Domain\Model\Schedule\ScheduleExceptionId;
use App\Scolarite\Domain\Model\Schedule\ScheduleExceptionType;
use App\Scolarite\Domain\Model\Schedule\ScheduleSlotId;
use App\Scolarite\Domain\Model\Schedule\TimeSlot;
use App\Scolarite\Domain\Repository\ScheduleExceptionRepository;
use App\Shared\Domain\Tenant\TenantId;

use function array_map;

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

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

    #[Override]
    public function save(ScheduleException $exception): void
    {
        $this->connection->executeStatement(
            'INSERT INTO schedule_exceptions (id, tenant_id, slot_id, exception_date, exception_type, new_start_time, new_end_time, new_room, new_teacher_id, reason, created_at, created_by)
             VALUES (:id, :tenant_id, :slot_id, :exception_date, :exception_type, :new_start_time, :new_end_time, :new_room, :new_teacher_id, :reason, :created_at, :created_by)
             ON CONFLICT (slot_id, exception_date) DO UPDATE SET
                 exception_type = EXCLUDED.exception_type,
                 new_start_time = EXCLUDED.new_start_time,
                 new_end_time = EXCLUDED.new_end_time,
                 new_room = EXCLUDED.new_room,
                 new_teacher_id = EXCLUDED.new_teacher_id,
                 reason = EXCLUDED.reason',
            [
                'id' => (string) $exception->id,
                'tenant_id' => (string) $exception->tenantId,
                'slot_id' => (string) $exception->slotId,
                'exception_date' => $exception->exceptionDate->format('Y-m-d'),
                'exception_type' => $exception->type->value,
                'new_start_time' => $exception->newTimeSlot?->startTime,
                'new_end_time' => $exception->newTimeSlot?->endTime,
                'new_room' => $exception->newRoom,
                'new_teacher_id' => $exception->newTeacherId !== null ? (string) $exception->newTeacherId : null,
                'reason' => $exception->reason,
                'created_at' => $exception->createdAt->format(DateTimeImmutable::ATOM),
                'created_by' => (string) $exception->createdBy,
            ],
        );
    }

    #[Override]
    public function findById(ScheduleExceptionId $id, TenantId $tenantId): ?ScheduleException
    {
        $row = $this->connection->fetchAssociative(
            'SELECT * FROM schedule_exceptions 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 findForSlotAndDate(
        ScheduleSlotId $slotId,
        DateTimeImmutable $date,
        TenantId $tenantId,
    ): ?ScheduleException {
        $row = $this->connection->fetchAssociative(
            'SELECT * FROM schedule_exceptions
             WHERE slot_id = :slot_id AND exception_date = :exception_date AND tenant_id = :tenant_id',
            [
                'slot_id' => (string) $slotId,
                'exception_date' => $date->format('Y-m-d'),
                'tenant_id' => (string) $tenantId,
            ],
        );

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

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

    #[Override]
    public function findForSlotBetweenDates(
        ScheduleSlotId $slotId,
        DateTimeImmutable $startDate,
        DateTimeImmutable $endDate,
        TenantId $tenantId,
    ): array {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT * FROM schedule_exceptions
             WHERE slot_id = :slot_id
             AND exception_date >= :start_date
             AND exception_date <= :end_date
             AND tenant_id = :tenant_id
             ORDER BY exception_date',
            [
                'slot_id' => (string) $slotId,
                'start_date' => $startDate->format('Y-m-d'),
                'end_date' => $endDate->format('Y-m-d'),
                'tenant_id' => (string) $tenantId,
            ],
        );

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

    #[Override]
    public function findForDateRange(
        TenantId $tenantId,
        DateTimeImmutable $startDate,
        DateTimeImmutable $endDate,
    ): array {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT * FROM schedule_exceptions
             WHERE tenant_id = :tenant_id
             AND exception_date >= :start_date
             AND exception_date <= :end_date
             ORDER BY exception_date',
            [
                'tenant_id' => (string) $tenantId,
                'start_date' => $startDate->format('Y-m-d'),
                'end_date' => $endDate->format('Y-m-d'),
            ],
        );

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

    #[Override]
    public function delete(ScheduleExceptionId $id, TenantId $tenantId): void
    {
        $this->connection->executeStatement(
            'DELETE FROM schedule_exceptions WHERE id = :id AND tenant_id = :tenant_id',
            ['id' => (string) $id, 'tenant_id' => (string) $tenantId],
        );
    }

    /**
     * @param array<string, mixed> $row
     */
    private function hydrate(array $row): ScheduleException
    {
        /** @var string $id */
        $id = $row['id'];
        /** @var string $tenantId */
        $tenantId = $row['tenant_id'];
        /** @var string $slotId */
        $slotId = $row['slot_id'];
        /** @var string $exceptionDate */
        $exceptionDate = $row['exception_date'];
        /** @var string $exceptionType */
        $exceptionType = $row['exception_type'];
        /** @var string|null $newStartTime */
        $newStartTime = $row['new_start_time'] ?? null;
        /** @var string|null $newEndTime */
        $newEndTime = $row['new_end_time'] ?? null;
        /** @var string|null $newRoom */
        $newRoom = $row['new_room'] ?? null;
        /** @var string|null $newTeacherId */
        $newTeacherId = $row['new_teacher_id'] ?? null;
        /** @var string|null $reason */
        $reason = $row['reason'] ?? null;
        /** @var string $createdBy */
        $createdBy = $row['created_by'];
        /** @var string $createdAt */
        $createdAt = $row['created_at'];

        return ScheduleException::reconstitute(
            id: ScheduleExceptionId::fromString($id),
            tenantId: TenantId::fromString($tenantId),
            slotId: ScheduleSlotId::fromString($slotId),
            exceptionDate: new DateTimeImmutable($exceptionDate),
            type: ScheduleExceptionType::from($exceptionType),
            newTimeSlot: $newStartTime !== null && $newEndTime !== null
                ? new TimeSlot($newStartTime, $newEndTime)
                : null,
            newRoom: $newRoom,
            newTeacherId: $newTeacherId !== null ? UserId::fromString($newTeacherId) : null,
            reason: $reason,
            createdBy: UserId::fromString($createdBy),
            createdAt: new DateTimeImmutable($createdAt),
        );
    }
}
