<?php

declare(strict_types=1);

namespace App\Administration\Application\Command\ImportTeachers;

use App\Administration\Application\Service\Import\MultiValueParser;
use App\Administration\Domain\Model\Import\ImportBatchId;
use App\Administration\Domain\Model\Import\TeacherImportField;
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
use App\Administration\Domain\Model\SchoolClass\ClassId;
use App\Administration\Domain\Model\SchoolClass\ClassName;
use App\Administration\Domain\Model\SchoolClass\SchoolId;
use App\Administration\Domain\Model\Subject\Subject;
use App\Administration\Domain\Model\Subject\SubjectCode;
use App\Administration\Domain\Model\Subject\SubjectId;
use App\Administration\Domain\Model\Subject\SubjectName;
use App\Administration\Domain\Model\TeacherAssignment\TeacherAssignment;
use App\Administration\Domain\Model\User\Email;
use App\Administration\Domain\Model\User\Role;
use App\Administration\Domain\Model\User\User;
use App\Administration\Domain\Repository\ClassRepository;
use App\Administration\Domain\Repository\SubjectRepository;
use App\Administration\Domain\Repository\TeacherAssignmentRepository;
use App\Administration\Domain\Repository\TeacherImportBatchRepository;
use App\Administration\Domain\Repository\UserRepository;
use App\Administration\Infrastructure\School\SchoolIdResolver;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use Doctrine\DBAL\Connection;
use DomainException;

use function in_array;
use function mb_strlen;

use Psr\Log\LoggerInterface;

use function sprintf;
use function strtoupper;
use function substr;

use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\MessageBusInterface;
use Throwable;

use function trim;

/**
 * Handler pour l'import d'enseignants en batch.
 *
 * Traite les lignes valides du batch, crée les enseignants et les affecte
 * aux matières/classes via TeacherAssignment.
 *
 * @see AC4: Import validé → enseignants créés
 */
#[AsMessageHandler(bus: 'command.bus')]
final readonly class ImportTeachersHandler
{
    private MultiValueParser $multiValueParser;

    public function __construct(
        private TeacherImportBatchRepository $teacherImportBatchRepository,
        private UserRepository $userRepository,
        private SubjectRepository $subjectRepository,
        private ClassRepository $classRepository,
        private TeacherAssignmentRepository $teacherAssignmentRepository,
        private SchoolIdResolver $schoolIdResolver,
        private Connection $connection,
        private Clock $clock,
        private LoggerInterface $logger,
        private MessageBusInterface $eventBus,
    ) {
        $this->multiValueParser = new MultiValueParser();
    }

    public function __invoke(ImportTeachersCommand $command): void
    {
        $batchId = ImportBatchId::fromString($command->batchId);
        $tenantId = TenantId::fromString($command->tenantId);
        $academicYearId = AcademicYearId::fromString($command->academicYearId);
        $schoolId = SchoolId::fromString($this->schoolIdResolver->resolveForTenant($command->tenantId));
        $now = $this->clock->now();

        $batch = $this->teacherImportBatchRepository->get($batchId);

        $batch->demarrer($now);
        $this->teacherImportBatchRepository->save($batch);

        $lignes = $batch->lignes();
        $importedCount = 0;
        $errorCount = 0;
        $processedCount = 0;

        try {
            /** @var array<string, SubjectId> $subjectCache */
            $subjectCache = [];
            /** @var list<string> $existingSubjectCodes */
            $existingSubjectCodes = [];
            /** @var list<string> $newlyCreatedCodes */
            $newlyCreatedCodes = [];

            foreach ($this->subjectRepository->findAllActiveByTenant($tenantId) as $subject) {
                $subjectCache[(string) $subject->name] = $subject->id;
                $existingSubjectCodes[] = (string) $subject->code;
            }

            /** @var array<string, ClassId> */
            $classCache = [];

            foreach ($lignes as $row) {
                try {
                    $firstName = trim($row->mappedData[TeacherImportField::FIRST_NAME->value] ?? '');
                    $lastName = trim($row->mappedData[TeacherImportField::LAST_NAME->value] ?? '');
                    $emailRaw = trim($row->mappedData[TeacherImportField::EMAIL->value] ?? '');
                    $subjectsRaw = $row->mappedData[TeacherImportField::SUBJECTS->value] ?? '';
                    $classesRaw = $row->mappedData[TeacherImportField::CLASSES->value] ?? '';

                    $emailVO = new Email($emailRaw);

                    $existingUser = $this->userRepository->findByEmail($emailVO, $tenantId);

                    if ($existingUser !== null && !$command->updateExisting) {
                        throw new DomainException(sprintf('L\'email "%s" est déjà utilisé.', $emailRaw));
                    }

                    $this->connection->beginTransaction();

                    try {
                        $subjects = $this->multiValueParser->parse($subjectsRaw);
                        $classes = $this->multiValueParser->parse($classesRaw);

                        $resolvedSubjectIds = $this->resolveSubjectIds(
                            $subjects,
                            $tenantId,
                            $schoolId,
                            $command->createMissingSubjects,
                            $now,
                            $subjectCache,
                            $existingSubjectCodes,
                            $newlyCreatedCodes,
                        );

                        $resolvedClassIds = $this->resolveClassIds(
                            $classes,
                            $tenantId,
                            $academicYearId,
                            $classCache,
                        );

                        if ($existingUser !== null) {
                            $existingUser->mettreAJourInfos($firstName, $lastName);
                            $this->userRepository->save($existingUser);

                            $this->addMissingAssignments(
                                $existingUser,
                                $resolvedSubjectIds,
                                $resolvedClassIds,
                                $tenantId,
                                $academicYearId,
                                $now,
                            );

                            $this->connection->commit();
                        } else {
                            $user = User::inviter(
                                email: $emailVO,
                                role: Role::PROF,
                                tenantId: $tenantId,
                                schoolName: $command->schoolName,
                                firstName: $firstName,
                                lastName: $lastName,
                                invitedAt: $now,
                            );

                            $this->userRepository->save($user);

                            foreach ($resolvedSubjectIds as $subjectId) {
                                foreach ($resolvedClassIds as $classId) {
                                    $assignment = TeacherAssignment::creer(
                                        tenantId: $tenantId,
                                        teacherId: $user->id,
                                        classId: $classId,
                                        subjectId: $subjectId,
                                        academicYearId: $academicYearId,
                                        createdAt: $now,
                                    );

                                    $this->teacherAssignmentRepository->save($assignment);
                                }
                            }

                            $this->connection->commit();

                            foreach ($user->pullDomainEvents() as $event) {
                                $this->eventBus->dispatch($event);
                            }
                        }
                    } catch (Throwable $e) {
                        $this->connection->rollBack();

                        throw $e;
                    }

                    ++$importedCount;
                } catch (DomainException $e) {
                    $this->logger->warning('Import enseignant ligne {line} échouée : {message}', [
                        'line' => $row->lineNumber,
                        'message' => $e->getMessage(),
                        'batch_id' => $command->batchId,
                    ]);
                    ++$errorCount;
                }

                ++$processedCount;
                if ($processedCount % 50 === 0) {
                    $batch->mettreAJourProgression($importedCount, $errorCount);
                    $this->teacherImportBatchRepository->save($batch);
                }
            }

            $batch->terminer($importedCount, $errorCount, $this->clock->now());
            $this->teacherImportBatchRepository->save($batch);
        } catch (Throwable $e) {
            $batch->echouer($errorCount, $this->clock->now());
            $this->teacherImportBatchRepository->save($batch);

            throw $e;
        }
    }

    /**
     * Ajoute les affectations manquantes pour un enseignant existant.
     *
     * @param list<SubjectId> $subjectIds
     * @param list<ClassId>   $classIds
     */
    private function addMissingAssignments(
        User $teacher,
        array $subjectIds,
        array $classIds,
        TenantId $tenantId,
        AcademicYearId $academicYearId,
        DateTimeImmutable $now,
    ): void {
        foreach ($subjectIds as $subjectId) {
            foreach ($classIds as $classId) {
                $existing = $this->teacherAssignmentRepository->findByTeacherClassSubject(
                    $teacher->id,
                    $classId,
                    $subjectId,
                    $academicYearId,
                    $tenantId,
                );

                if ($existing !== null) {
                    continue;
                }

                $assignment = TeacherAssignment::creer(
                    tenantId: $tenantId,
                    teacherId: $teacher->id,
                    classId: $classId,
                    subjectId: $subjectId,
                    academicYearId: $academicYearId,
                    createdAt: $now,
                );

                $this->teacherAssignmentRepository->save($assignment);
            }
        }
    }

    /**
     * @param list<string>             $subjectNames
     * @param array<string, SubjectId> $cache
     * @param list<string>             $existingCodes
     * @param list<string>             $newlyCreatedCodes
     *
     * @return list<SubjectId>
     */
    private function resolveSubjectIds(
        array $subjectNames,
        TenantId $tenantId,
        SchoolId $schoolId,
        bool $createMissing,
        DateTimeImmutable $now,
        array &$cache,
        array &$existingCodes,
        array &$newlyCreatedCodes,
    ): array {
        $ids = [];

        foreach ($subjectNames as $name) {
            if (isset($cache[$name])) {
                $ids[] = $cache[$name];
                continue;
            }

            if ($createMissing) {
                $subjectId = $this->createSubject($name, $tenantId, $schoolId, $now, $existingCodes, $newlyCreatedCodes);
                $cache[$name] = $subjectId;
                $ids[] = $subjectId;
            }
        }

        return $ids;
    }

    /**
     * @param list<string>           $classNames
     * @param array<string, ClassId> $cache
     *
     * @return list<ClassId>
     */
    private function resolveClassIds(
        array $classNames,
        TenantId $tenantId,
        AcademicYearId $academicYearId,
        array &$cache,
    ): array {
        $ids = [];

        foreach ($classNames as $name) {
            if (isset($cache[$name])) {
                $ids[] = $cache[$name];
                continue;
            }

            $classNameVO = new ClassName($name);
            $class = $this->classRepository->findByName($classNameVO, $tenantId, $academicYearId);

            if ($class !== null) {
                $cache[$name] = $class->id;
                $ids[] = $class->id;
            }
        }

        return $ids;
    }

    /**
     * @param list<string> $existingCodes
     * @param list<string> $newlyCreatedCodes
     */
    private function createSubject(
        string $name,
        TenantId $tenantId,
        SchoolId $schoolId,
        DateTimeImmutable $now,
        array &$existingCodes,
        array &$newlyCreatedCodes,
    ): SubjectId {
        if (trim($name) === '') {
            throw new DomainException('Le nom de la matière ne peut pas être vide.');
        }

        $code = $this->generateUniqueSubjectCode($name, $existingCodes, $newlyCreatedCodes);

        if ($code === '') {
            throw new DomainException(sprintf('Impossible de générer un code pour la matière "%s".', $name));
        }

        $subject = Subject::creer(
            tenantId: $tenantId,
            schoolId: $schoolId,
            name: new SubjectName($name),
            code: new SubjectCode($code),
            color: null,
            createdAt: $now,
        );

        $this->subjectRepository->save($subject);
        $newlyCreatedCodes[] = $code;

        return $subject->id;
    }

    /**
     * @param list<string> $existingCodes
     * @param list<string> $newlyCreatedCodes
     */
    private function generateUniqueSubjectCode(string $name, array $existingCodes, array $newlyCreatedCodes): string
    {
        $base = strtoupper(substr(trim($name), 0, 4));

        if (mb_strlen($base) < 2) {
            $base .= 'XX';
        }

        $allCodes = [...$existingCodes, ...$newlyCreatedCodes];

        if (!in_array($base, $allCodes, true)) {
            return $base;
        }

        for ($i = 2; $i <= 99; ++$i) {
            $candidate = $base . $i;
            if (!in_array($candidate, $allCodes, true)) {
                return $candidate;
            }
        }

        return $base;
    }
}
