<?php

declare(strict_types=1);

namespace App\Administration\Domain\Model\GradingConfiguration;

use App\Administration\Domain\Event\ModeNotationConfigure;
use App\Administration\Domain\Exception\CannotChangeGradingModeWithExistingGradesException;
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
use App\Administration\Domain\Model\SchoolClass\SchoolId;
use App\Shared\Domain\AggregateRoot;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;

/**
 * Aggregate Root associant un mode de notation à un établissement pour une année scolaire.
 *
 * Le mode de notation détermine comment les enseignants évaluent les élèves :
 * numériquement, par lettres, par compétences ou sans notes du tout.
 *
 * Invariant : le mode ne peut pas être changé si des notes existent déjà.
 *
 * @see FR24: Supporter différentes pédagogies
 */
final class SchoolGradingConfiguration extends AggregateRoot
{
    public const GradingMode DEFAULT_MODE = GradingMode::NUMERIC_20;

    public private(set) DateTimeImmutable $updatedAt;

    private function __construct(
        public private(set) SchoolGradingConfigurationId $id,
        public private(set) TenantId $tenantId,
        public private(set) SchoolId $schoolId,
        public private(set) AcademicYearId $academicYearId,
        public private(set) GradingConfiguration $gradingConfiguration,
        public private(set) DateTimeImmutable $configuredAt,
    ) {
        $this->updatedAt = $configuredAt;
    }

    public static function generateId(): SchoolGradingConfigurationId
    {
        return SchoolGradingConfigurationId::generate();
    }

    /**
     * Configure le mode de notation pour un établissement et une année scolaire.
     *
     * @param bool $hasExistingGrades Résultat de la vérification par l'Application Layer
     */
    public static function configurer(
        TenantId $tenantId,
        SchoolId $schoolId,
        AcademicYearId $academicYearId,
        GradingMode $mode,
        bool $hasExistingGrades,
        DateTimeImmutable $configuredAt,
    ): self {
        // Le mode par défaut (NUMERIC_20) est autorisé même avec des notes existantes :
        // c'est le mode implicite avant toute configuration, donc le créer explicitement
        // est une opération idempotente. En revanche, changerMode() bloque tout changement
        // dès qu'il y a des notes, quel que soit le mode cible.
        if ($hasExistingGrades && $mode !== self::DEFAULT_MODE) {
            throw new CannotChangeGradingModeWithExistingGradesException();
        }

        $config = new self(
            id: SchoolGradingConfigurationId::generate(),
            tenantId: $tenantId,
            schoolId: $schoolId,
            academicYearId: $academicYearId,
            gradingConfiguration: new GradingConfiguration($mode),
            configuredAt: $configuredAt,
        );

        $config->recordEvent(new ModeNotationConfigure(
            configurationId: $config->id,
            tenantId: $config->tenantId,
            schoolId: $config->schoolId,
            academicYearId: $config->academicYearId,
            mode: $mode,
            occurredOn: $configuredAt,
        ));

        return $config;
    }

    /**
     * Change le mode de notation.
     *
     * @param bool $hasExistingGrades Résultat de la vérification par l'Application Layer
     */
    public function changerMode(
        GradingMode $nouveauMode,
        bool $hasExistingGrades,
        DateTimeImmutable $at,
    ): void {
        if ($this->gradingConfiguration->mode === $nouveauMode) {
            return;
        }

        if ($hasExistingGrades) {
            throw new CannotChangeGradingModeWithExistingGradesException();
        }

        $this->gradingConfiguration = new GradingConfiguration($nouveauMode);
        $this->updatedAt = $at;

        $this->recordEvent(new ModeNotationConfigure(
            configurationId: $this->id,
            tenantId: $this->tenantId,
            schoolId: $this->schoolId,
            academicYearId: $this->academicYearId,
            mode: $nouveauMode,
            occurredOn: $at,
        ));
    }

    /**
     * @internal Pour usage Infrastructure uniquement
     */
    public static function reconstitute(
        SchoolGradingConfigurationId $id,
        TenantId $tenantId,
        SchoolId $schoolId,
        AcademicYearId $academicYearId,
        GradingConfiguration $gradingConfiguration,
        DateTimeImmutable $configuredAt,
        DateTimeImmutable $updatedAt,
    ): self {
        $config = new self(
            id: $id,
            tenantId: $tenantId,
            schoolId: $schoolId,
            academicYearId: $academicYearId,
            gradingConfiguration: $gradingConfiguration,
            configuredAt: $configuredAt,
        );

        $config->updatedAt = $updatedAt;

        return $config;
    }
}
