<?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\RemplacementNotFoundException;
use App\Scolarite\Domain\Model\TeacherReplacement\ClassSubjectPair;
use App\Scolarite\Domain\Model\TeacherReplacement\ReplacementStatus;
use App\Scolarite\Domain\Model\TeacherReplacement\TeacherReplacement;
use App\Scolarite\Domain\Model\TeacherReplacement\TeacherReplacementId;
use App\Scolarite\Domain\Repository\TeacherReplacementRepository;
use App\Shared\Domain\Tenant\TenantId;

use function count;

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

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

    #[Override]
    public function save(TeacherReplacement $replacement): void
    {
        $this->connection->executeStatement(
            'INSERT INTO teacher_replacements (id, tenant_id, replaced_teacher_id, replacement_teacher_id, start_date, end_date, status, reason, created_by, created_at, ended_at, updated_at)
             VALUES (:id, :tenant_id, :replaced_teacher_id, :replacement_teacher_id, :start_date, :end_date, :status, :reason, :created_by, :created_at, :ended_at, :updated_at)
             ON CONFLICT (id) DO UPDATE SET
                 status = EXCLUDED.status,
                 ended_at = EXCLUDED.ended_at,
                 updated_at = EXCLUDED.updated_at',
            [
                'id' => (string) $replacement->id,
                'tenant_id' => (string) $replacement->tenantId,
                'replaced_teacher_id' => (string) $replacement->replacedTeacherId,
                'replacement_teacher_id' => (string) $replacement->replacementTeacherId,
                'start_date' => $replacement->startDate->format('Y-m-d'),
                'end_date' => $replacement->endDate->format('Y-m-d'),
                'status' => $replacement->status->value,
                'reason' => $replacement->reason,
                'created_by' => (string) $replacement->createdBy,
                'created_at' => $replacement->createdAt->format(DateTimeImmutable::ATOM),
                'ended_at' => $replacement->endedAt?->format(DateTimeImmutable::ATOM),
                'updated_at' => $replacement->updatedAt->format(DateTimeImmutable::ATOM),
            ],
        );

        // Upsert replacement_classes
        $this->connection->executeStatement(
            'DELETE FROM replacement_classes WHERE replacement_id = :replacement_id',
            ['replacement_id' => (string) $replacement->id],
        );

        foreach ($replacement->classes as $pair) {
            $this->connection->executeStatement(
                'INSERT INTO replacement_classes (replacement_id, class_id, subject_id) VALUES (:replacement_id, :class_id, :subject_id)',
                [
                    'replacement_id' => (string) $replacement->id,
                    'class_id' => (string) $pair->classId,
                    'subject_id' => (string) $pair->subjectId,
                ],
            );
        }
    }

    #[Override]
    public function get(TeacherReplacementId $id, TenantId $tenantId): TeacherReplacement
    {
        $replacement = $this->findById($id, $tenantId);

        if ($replacement === null) {
            throw RemplacementNotFoundException::withId($id);
        }

        return $replacement;
    }

    #[Override]
    public function findById(TeacherReplacementId $id, TenantId $tenantId): ?TeacherReplacement
    {
        $row = $this->connection->fetchAssociative(
            'SELECT * FROM teacher_replacements 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 findActiveReplacement(
        UserId $replacementTeacherId,
        ClassId $classId,
        SubjectId $subjectId,
        DateTimeImmutable $at,
        TenantId $tenantId,
    ): ?TeacherReplacement {
        $row = $this->connection->fetchAssociative(
            'SELECT tr.* FROM teacher_replacements tr
             JOIN replacement_classes rc ON rc.replacement_id = tr.id
             WHERE tr.tenant_id = :tenant_id
             AND tr.replacement_teacher_id = :replacement_teacher_id
             AND rc.class_id = :class_id
             AND rc.subject_id = :subject_id
             AND tr.status = :status
             AND tr.start_date <= :at
             AND tr.end_date >= :at',
            [
                'tenant_id' => (string) $tenantId,
                'replacement_teacher_id' => (string) $replacementTeacherId,
                'class_id' => (string) $classId,
                'subject_id' => (string) $subjectId,
                'status' => ReplacementStatus::ACTIVE->value,
                'at' => $at->format('Y-m-d'),
            ],
        );

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

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

    #[Override]
    public function findActiveByTenant(TenantId $tenantId, DateTimeImmutable $at): array
    {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT * FROM teacher_replacements
             WHERE tenant_id = :tenant_id
             AND status = :status
             AND start_date <= :at
             AND end_date >= :at
             ORDER BY start_date ASC',
            [
                'tenant_id' => (string) $tenantId,
                'status' => ReplacementStatus::ACTIVE->value,
                'at' => $at->format('Y-m-d'),
            ],
        );

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

    #[Override]
    public function findActiveByReplacementTeacher(
        UserId $replacementTeacherId,
        TenantId $tenantId,
        DateTimeImmutable $at,
    ): array {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT * FROM teacher_replacements
             WHERE replacement_teacher_id = :replacement_teacher_id
             AND tenant_id = :tenant_id
             AND status = :status
             AND start_date <= :at
             AND end_date >= :at
             ORDER BY created_at ASC',
            [
                'replacement_teacher_id' => (string) $replacementTeacherId,
                'tenant_id' => (string) $tenantId,
                'status' => ReplacementStatus::ACTIVE->value,
                'at' => $at->format('Y-m-d'),
            ],
        );

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

    #[Override]
    public function findExpired(DateTimeImmutable $at): array
    {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT * FROM teacher_replacements
             WHERE status = :status
             AND end_date < :at
             ORDER BY end_date ASC',
            [
                'status' => ReplacementStatus::ACTIVE->value,
                'at' => $at->format('Y-m-d'),
            ],
        );

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

    /** @return array<ClassSubjectPair> */
    private function loadClasses(string $replacementId): array
    {
        return $this->loadClassesBatch([$replacementId])[$replacementId] ?? [];
    }

    /**
     * @param list<string> $replacementIds
     *
     * @return array<string, list<ClassSubjectPair>>
     */
    private function loadClassesBatch(array $replacementIds): array
    {
        if ($replacementIds === []) {
            return [];
        }

        $placeholders = implode(', ', array_fill(0, count($replacementIds), '?'));
        $rows = $this->connection->fetchAllAssociative(
            "SELECT replacement_id, class_id, subject_id FROM replacement_classes WHERE replacement_id IN ($placeholders)",
            array_values($replacementIds),
        );

        $grouped = [];
        foreach ($rows as $row) {
            /** @var string $replacementId */
            $replacementId = $row['replacement_id'];
            /** @var string $classId */
            $classId = $row['class_id'];
            /** @var string $subjectId */
            $subjectId = $row['subject_id'];

            $grouped[$replacementId][] = new ClassSubjectPair(
                ClassId::fromString($classId),
                SubjectId::fromString($subjectId),
            );
        }

        return $grouped;
    }

    /**
     * @param list<array<string, mixed>> $rows
     *
     * @return list<TeacherReplacement>
     */
    private function hydrateMany(array $rows): array
    {
        if ($rows === []) {
            return [];
        }

        $ids = array_map(static function (array $row): string {
            /** @var string $id */
            $id = $row['id'];

            return $id;
        }, $rows);
        $classesByReplacement = $this->loadClassesBatch($ids);

        return array_map(
            function (array $row) use ($classesByReplacement) {
                /** @var string $id */
                $id = $row['id'];

                return $this->hydrate($row, $classesByReplacement[$id] ?? []);
            },
            $rows,
        );
    }

    /**
     * @param array<string, mixed>         $row
     * @param array<ClassSubjectPair>|null $classes Pre-loaded classes (null = load individually)
     */
    private function hydrate(array $row, ?array $classes = null): TeacherReplacement
    {
        /** @var string $id */
        $id = $row['id'];
        /** @var string $tenantId */
        $tenantId = $row['tenant_id'];
        /** @var string $replacedTeacherId */
        $replacedTeacherId = $row['replaced_teacher_id'];
        /** @var string $replacementTeacherId */
        $replacementTeacherId = $row['replacement_teacher_id'];
        /** @var string $startDate */
        $startDate = $row['start_date'];
        /** @var string $endDate */
        $endDate = $row['end_date'];
        /** @var string $status */
        $status = $row['status'];
        /** @var string|null $reason */
        $reason = $row['reason'];
        /** @var string $createdBy */
        $createdBy = $row['created_by'];
        /** @var string $createdAt */
        $createdAt = $row['created_at'];
        /** @var string|null $endedAt */
        $endedAt = $row['ended_at'];
        /** @var string $updatedAt */
        $updatedAt = $row['updated_at'];

        return TeacherReplacement::reconstitute(
            id: TeacherReplacementId::fromString($id),
            tenantId: TenantId::fromString($tenantId),
            replacedTeacherId: UserId::fromString($replacedTeacherId),
            replacementTeacherId: UserId::fromString($replacementTeacherId),
            startDate: new DateTimeImmutable($startDate),
            endDate: new DateTimeImmutable($endDate),
            status: ReplacementStatus::from($status),
            classes: $classes ?? $this->loadClasses($id),
            reason: $reason,
            createdBy: UserId::fromString($createdBy),
            createdAt: new DateTimeImmutable($createdAt),
            endedAt: $endedAt !== null ? new DateTimeImmutable($endedAt) : null,
            updatedAt: new DateTimeImmutable($updatedAt),
        );
    }
}
