<?php

declare(strict_types=1);

namespace App\Administration\Domain\Model\User;

use App\Administration\Domain\Event\CompteActive;
use App\Administration\Domain\Event\CompteCreated;
use App\Administration\Domain\Event\InvitationRenvoyee;
use App\Administration\Domain\Event\MotDePasseChange;
use App\Administration\Domain\Event\RoleAttribue;
use App\Administration\Domain\Event\RoleRetire;
use App\Administration\Domain\Event\UtilisateurBloque;
use App\Administration\Domain\Event\UtilisateurDebloque;
use App\Administration\Domain\Event\UtilisateurInvite;
use App\Administration\Domain\Exception\CompteNonActivableException;
use App\Administration\Domain\Exception\DernierRoleNonRetirableException;
use App\Administration\Domain\Exception\RoleDejaAttribueException;
use App\Administration\Domain\Exception\RoleNonAttribueException;
use App\Administration\Domain\Exception\UtilisateurDejaInviteException;
use App\Administration\Domain\Exception\UtilisateurNonBlocableException;
use App\Administration\Domain\Exception\UtilisateurNonDeblocableException;
use App\Administration\Domain\Model\ConsentementParental\ConsentementParental;
use App\Administration\Domain\Policy\ConsentementParentalPolicy;
use App\Shared\Domain\AggregateRoot;
use App\Shared\Domain\Tenant\TenantId;

use function array_values;
use function count;

use DateTimeImmutable;

use function in_array;

use InvalidArgumentException;

/**
 * Aggregate Root representing a user in Classeo.
 *
 * A user belongs to a school (tenant) and can have multiple roles (FR5).
 * The account lifecycle goes through multiple statuses: creation → activation.
 * Minors (< 15 years) require parental consent before activation.
 */
final class User extends AggregateRoot
{
    public private(set) ?string $hashedPassword = null;
    public private(set) ?DateTimeImmutable $activatedAt = null;
    public private(set) ?ConsentementParental $consentementParental = null;
    public private(set) ?DateTimeImmutable $invitedAt = null;
    public private(set) ?DateTimeImmutable $blockedAt = null;
    public private(set) ?string $blockedReason = null;

    /** @var Role[] */
    public private(set) array $roles;

    /**
     * @param Role[] $roles
     */
    private function __construct(
        public private(set) UserId $id,
        public private(set) Email $email,
        array $roles,
        public private(set) TenantId $tenantId,
        public private(set) string $schoolName,
        public private(set) StatutCompte $statut,
        public private(set) ?DateTimeImmutable $dateNaissance,
        public private(set) DateTimeImmutable $createdAt,
        public private(set) string $firstName = '',
        public private(set) string $lastName = '',
    ) {
        $this->roles = $roles;
    }

    /**
     * Returns the primary role (first assigned role) for backward compatibility.
     */
    public Role $role {
        get => $this->roles[0];
    }

    /**
     * Creates a new user account awaiting activation.
     */
    public static function creer(
        Email $email,
        Role $role,
        TenantId $tenantId,
        string $schoolName,
        ?DateTimeImmutable $dateNaissance,
        DateTimeImmutable $createdAt,
    ): self {
        $user = new self(
            id: UserId::generate(),
            email: $email,
            roles: [$role],
            tenantId: $tenantId,
            schoolName: $schoolName,
            statut: StatutCompte::EN_ATTENTE,
            dateNaissance: $dateNaissance,
            createdAt: $createdAt,
        );

        $user->recordEvent(new CompteCreated(
            userId: $user->id,
            email: (string) $user->email,
            role: $user->role->value,
            tenantId: $user->tenantId,
            occurredOn: $createdAt,
        ));

        return $user;
    }

    /**
     * Assigns an additional role to the user.
     *
     * @throws RoleDejaAttribueException if the role is already assigned
     */
    public function attribuerRole(Role $role, DateTimeImmutable $at): void
    {
        if ($this->aLeRole($role)) {
            throw RoleDejaAttribueException::pour($this->id, $role);
        }

        $this->roles = [...$this->roles, $role];

        $this->recordEvent(new RoleAttribue(
            userId: $this->id,
            email: (string) $this->email,
            role: $role->value,
            tenantId: $this->tenantId,
            occurredOn: $at,
        ));
    }

    /**
     * Removes a role from the user.
     *
     * @throws RoleNonAttribueException         if the role is not assigned
     * @throws DernierRoleNonRetirableException if this is the last role
     */
    public function retirerRole(Role $role, DateTimeImmutable $at): void
    {
        if (!$this->aLeRole($role)) {
            throw RoleNonAttribueException::pour($this->id, $role);
        }

        if (count($this->roles) === 1) {
            throw DernierRoleNonRetirableException::pour($this->id);
        }

        $this->roles = array_values(
            array_filter($this->roles, static fn (Role $r) => $r !== $role),
        );

        $this->recordEvent(new RoleRetire(
            userId: $this->id,
            email: (string) $this->email,
            role: $role->value,
            tenantId: $this->tenantId,
            occurredOn: $at,
        ));
    }

    /**
     * Checks if the user has a specific role.
     */
    public function aLeRole(Role $role): bool
    {
        return in_array($role, $this->roles, true);
    }

    /**
     * Returns the primary role (first assigned).
     */
    public function rolePrincipal(): Role
    {
        return $this->roles[0];
    }

    /**
     * Activates the account with the hashed password.
     *
     * @throws CompteNonActivableException if the account cannot be activated
     */
    public function activer(
        string $hashedPassword,
        DateTimeImmutable $at,
        ConsentementParentalPolicy $consentementPolicy,
    ): void {
        if (!$this->statut->peutActiver()) {
            throw CompteNonActivableException::carStatutIncompatible($this->id, $this->statut);
        }

        // Check if parental consent is required
        if ($consentementPolicy->estRequis($this->dateNaissance)) {
            if ($this->consentementParental === null) {
                throw CompteNonActivableException::carConsentementManquant($this->id);
            }
        }

        $this->hashedPassword = $hashedPassword;
        $this->statut = StatutCompte::ACTIF;
        $this->activatedAt = $at;

        $this->recordEvent(new CompteActive(
            userId: (string) $this->id,
            email: (string) $this->email,
            tenantId: $this->tenantId,
            role: $this->role->value,
            occurredOn: $at,
            aggregateId: $this->id->value,
        ));
    }

    /**
     * Records the parental consent given by the parent.
     */
    public function enregistrerConsentementParental(ConsentementParental $consentement): void
    {
        $this->consentementParental = $consentement;

        // If the account was awaiting consent, move to awaiting activation
        if ($this->statut === StatutCompte::CONSENTEMENT_REQUIS) {
            $this->statut = StatutCompte::EN_ATTENTE;
        }
    }

    /**
     * Checks if this user is a minor and requires parental consent.
     */
    public function necessiteConsentementParental(ConsentementParentalPolicy $policy): bool
    {
        return $policy->estRequis($this->dateNaissance);
    }

    /**
     * Checks if the account is active and can log in.
     */
    public function peutSeConnecter(): bool
    {
        return $this->statut->peutSeConnecter();
    }

    /**
     * Creates a new user via admin invitation.
     *
     * Unlike creer() which is for self-registration, inviter() is used
     * when an admin creates a user account from the management interface.
     */
    public static function inviter(
        Email $email,
        Role $role,
        TenantId $tenantId,
        string $schoolName,
        string $firstName,
        string $lastName,
        DateTimeImmutable $invitedAt,
        ?DateTimeImmutable $dateNaissance = null,
        ?string $studentId = null,
        ?string $relationshipType = null,
    ): self {
        $user = new self(
            id: UserId::generate(),
            email: $email,
            roles: [$role],
            tenantId: $tenantId,
            schoolName: $schoolName,
            statut: StatutCompte::EN_ATTENTE,
            dateNaissance: $dateNaissance,
            createdAt: $invitedAt,
            firstName: $firstName,
            lastName: $lastName,
        );

        $user->invitedAt = $invitedAt;

        $user->recordEvent(new UtilisateurInvite(
            userId: $user->id,
            email: (string) $user->email,
            role: $user->role->value,
            firstName: $firstName,
            lastName: $lastName,
            tenantId: $user->tenantId,
            occurredOn: $invitedAt,
            studentId: $studentId,
            relationshipType: $relationshipType,
        ));

        return $user;
    }

    /**
     * Resends the invitation for a user still awaiting activation.
     *
     * @throws UtilisateurDejaInviteException if user is no longer in a pending state
     */
    public function renvoyerInvitation(DateTimeImmutable $at): void
    {
        if ($this->statut !== StatutCompte::EN_ATTENTE && $this->statut !== StatutCompte::CONSENTEMENT_REQUIS) {
            throw UtilisateurDejaInviteException::carStatutIncompatible($this->id, $this->statut);
        }

        $this->invitedAt = $at;

        $this->recordEvent(new InvitationRenvoyee(
            userId: $this->id,
            email: (string) $this->email,
            tenantId: $this->tenantId,
            occurredOn: $at,
        ));
    }

    /**
     * Blocks a user account.
     *
     * @throws UtilisateurNonBlocableException if user is already suspended or archived
     */
    public function bloquer(string $reason, DateTimeImmutable $at): void
    {
        if ($this->statut !== StatutCompte::ACTIF) {
            throw UtilisateurNonBlocableException::carStatutIncompatible($this->id, $this->statut);
        }

        $this->statut = StatutCompte::SUSPENDU;
        $this->blockedAt = $at;
        $this->blockedReason = $reason;

        $this->recordEvent(new UtilisateurBloque(
            userId: $this->id,
            email: (string) $this->email,
            reason: $reason,
            tenantId: $this->tenantId,
            occurredOn: $at,
        ));
    }

    /**
     * Unblocks a suspended user account, restoring it to active status.
     *
     * @throws UtilisateurNonDeblocableException if user is not suspended
     */
    public function debloquer(DateTimeImmutable $at): void
    {
        if ($this->statut !== StatutCompte::SUSPENDU) {
            throw UtilisateurNonDeblocableException::carStatutIncompatible($this->id, $this->statut);
        }

        $this->statut = StatutCompte::ACTIF;
        $this->blockedAt = null;
        $this->blockedReason = null;

        $this->recordEvent(new UtilisateurDebloque(
            userId: $this->id,
            email: (string) $this->email,
            tenantId: $this->tenantId,
            occurredOn: $at,
        ));
    }

    /**
     * Checks if the invitation has expired (> 7 days since last invitation).
     */
    public function estInvitationExpiree(DateTimeImmutable $at): bool
    {
        if ($this->statut !== StatutCompte::EN_ATTENTE && $this->statut !== StatutCompte::CONSENTEMENT_REQUIS) {
            return false;
        }

        if ($this->invitedAt === null) {
            return false;
        }

        return $at > $this->invitedAt->modify('+7 days');
    }

    /**
     * Changes the user's password.
     *
     * Used during password reset.
     */
    public function changerMotDePasse(string $hashedPassword, DateTimeImmutable $at): void
    {
        $this->hashedPassword = $hashedPassword;

        $this->recordEvent(new MotDePasseChange(
            userId: (string) $this->id,
            email: (string) $this->email,
            tenantId: $this->tenantId,
            occurredOn: $at,
        ));
    }

    /**
     * Reconstitutes a User from storage.
     *
     * @param Role[] $roles
     *
     * @internal For Infrastructure use only
     */
    public static function reconstitute(
        UserId $id,
        Email $email,
        array $roles,
        TenantId $tenantId,
        string $schoolName,
        StatutCompte $statut,
        ?DateTimeImmutable $dateNaissance,
        DateTimeImmutable $createdAt,
        ?string $hashedPassword,
        ?DateTimeImmutable $activatedAt,
        ?ConsentementParental $consentementParental,
        string $firstName = '',
        string $lastName = '',
        ?DateTimeImmutable $invitedAt = null,
        ?DateTimeImmutable $blockedAt = null,
        ?string $blockedReason = null,
    ): self {
        if ($roles === []) {
            throw new InvalidArgumentException('Un utilisateur doit avoir au moins un rôle.');
        }

        $user = new self(
            id: $id,
            email: $email,
            roles: $roles,
            tenantId: $tenantId,
            schoolName: $schoolName,
            statut: $statut,
            dateNaissance: $dateNaissance,
            createdAt: $createdAt,
            firstName: $firstName,
            lastName: $lastName,
        );

        $user->hashedPassword = $hashedPassword;
        $user->activatedAt = $activatedAt;
        $user->consentementParental = $consentementParental;
        $user->invitedAt = $invitedAt;
        $user->blockedAt = $blockedAt;
        $user->blockedReason = $blockedReason;

        return $user;
    }
}
