<?php

namespace Mautic\UserBundle\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use Doctrine\ORM\Mapping as ORM;
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\CoreBundle\Entity\CacheInvalidateInterface;
use Mautic\CoreBundle\Entity\FormEntity;
use Mautic\UserBundle\Form\Validator\Constraints\NotWeak;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Form\Form;
use Symfony\Component\Security\Core\User\EquatableInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Mapping\ClassMetadata;

#[ApiResource(
    shortName: 'User',
    operations: [
        new GetCollection(uriTemplate: '/users', security: "is_granted('user:users:viewown')"),
        new Post(uriTemplate: '/users', security: "is_granted('user:users:create')", processor: \Mautic\UserBundle\ApiPlatform\UserProcessor::class),
        new Get(uriTemplate: '/users/{id}', security: "is_granted('user:users:viewown')"),
        new Put(uriTemplate: '/users/{id}', security: "is_granted('user:users:editown')", processor: \Mautic\UserBundle\ApiPlatform\UserProcessor::class),
        new Patch(uriTemplate: '/users/{id}', security: "is_granted('user:users:editother')", processor: \Mautic\UserBundle\ApiPlatform\UserProcessor::class),
        new Delete(uriTemplate: '/users/{id}', security: "is_granted('user:users:deleteown')"),
    ],
    normalizationContext: [
        'groups'                  => ['user:read'],
        'swagger_definition_name' => 'Read',
    ],
    denormalizationContext: [
        'groups'                  => ['user:write'],
        'swagger_definition_name' => 'Write',
    ]
)]
class User extends FormEntity implements UserInterface, EquatableInterface, PasswordAuthenticatedUserInterface, CacheInvalidateInterface
{
    public const CACHE_NAMESPACE = 'User';

    /**
     * @var ?int
     */
    #[Groups(['user:read'])]
    protected $id;

    #[Groups(['user:read', 'user:write'])]
    protected ?string $username = null;

    /**
     * @var string
     */
    protected $password;

    /**
     * Used for when updating the password.
     *
     * @var ?string
     */
    #[Groups(['user:write'])]
    private $plainPassword;

    /**
     * Used for updating account.
     *
     * @var ?string
     */
    private $currentPassword;

    /**
     * @var string
     */
    #[Groups(['user:read', 'user:write'])]
    private $firstName;

    /**
     * @var string
     */
    #[Groups(['user:read', 'user:write'])]
    private $lastName;

    /**
     * @var string
     */
    #[Groups(['user:read', 'user:write'])]
    private $email;

    /**
     * @var string|null
     */
    #[Groups(['user:read', 'user:write'])]
    private $position;

    /**
     * @var Role
     */
    #[Groups(['user:read', 'user:write'])]
    private $role;

    /**
     * @var string|null
     */
    #[Groups(['user:read', 'user:write'])]
    private $timezone = '';

    /**
     * @var string|null
     */
    #[Groups(['user:read', 'user:write'])]
    private $locale = '';

    /**
     * @var \DateTimeInterface
     */
    #[Groups(['user:read'])]
    private $lastLogin;

    /**
     * @var \DateTimeInterface
     */
    #[Groups(['user:read'])]
    private $lastActive;

    /**
     * Stores active role permissions.
     */
    private $activePermissions;

    /**
     * @var array
     */
    #[Groups(['user:read', 'user:write'])]
    private $preferences = [];

    /**
     * @var string|null
     */
    #[Groups(['user:read', 'user:write'])]
    private $signature;

    /**
     * @param bool $guest
     */
    public function __construct(
        private $guest = false,
    ) {
    }

    public static function loadMetadata(ORM\ClassMetadata $metadata): void
    {
        $builder = new ClassMetadataBuilder($metadata);

        $builder->setTable('users')
            ->setCustomRepositoryClass(UserRepository::class);

        $builder->addId();

        $builder->createField('username', 'string')
            ->length(191)
            ->unique()
            ->build();

        $builder->createField('password', 'string')
            ->length(64)
            ->build();

        $builder->createField('firstName', 'string')
            ->columnName('first_name')
            ->length(191)
            ->build();

        $builder->createField('lastName', 'string')
            ->columnName('last_name')
            ->length(191)
            ->build();

        $builder->createField('email', 'string')
            ->length(191)
            ->unique()
            ->build();

        $builder->createField('position', 'string')
            ->length(191)
            ->nullable()
            ->build();

        $builder->createManyToOne('role', 'Role')
            ->inversedBy('users')
            ->cascadeMerge()
            ->addJoinColumn('role_id', 'id', false)
            ->build();

        $builder->createField('timezone', 'string')
            ->nullable()
            ->build();

        $builder->createField('locale', 'string')
            ->nullable()
            ->build();

        $builder->createField('lastLogin', 'datetime')
            ->columnName('last_login')
            ->nullable()
            ->build();

        $builder->createField('lastActive', 'datetime')
            ->columnName('last_active')
            ->nullable()
            ->build();

        $builder->createField('preferences', 'array')
            ->nullable()
            ->build();

        $builder->createField('signature', 'text')
            ->nullable()
            ->build();
    }

    public static function loadValidatorMetadata(ClassMetadata $metadata): void
    {
        $metadata->addPropertyConstraint('username', new Assert\NotBlank(
            ['message' => 'mautic.user.user.username.notblank']
        ));

        $metadata->addConstraint(new UniqueEntity(
            [
                'fields'           => ['username'],
                'message'          => 'mautic.user.user.username.unique',
                'repositoryMethod' => 'checkUniqueUsernameEmail',
            ]
        ));

        $metadata->addPropertyConstraint('firstName', new Assert\NotBlank(
            ['message' => 'mautic.user.user.firstname.notblank']
        ));

        $metadata->addPropertyConstraint('lastName', new Assert\NotBlank(
            ['message' => 'mautic.user.user.lastname.notblank']
        ));

        $metadata->addPropertyConstraint('email', new Assert\NotBlank(
            ['message' => 'mautic.user.user.email.valid']
        ));

        $metadata->addPropertyConstraint('email', new Assert\Email(
            [
                'message' => 'mautic.user.user.email.valid',
                'groups'  => ['SecondPass'],
            ]
        ));

        $metadata->addConstraint(new UniqueEntity(
            [
                'fields'           => ['email'],
                'message'          => 'mautic.user.user.email.unique',
                'repositoryMethod' => 'checkUniqueUsernameEmail',
            ]
        ));

        $metadata->addPropertyConstraint('role', new Assert\NotBlank(
            ['message' => 'mautic.user.user.role.notblank']
        ));

        $metadata->addPropertyConstraint('plainPassword', new Assert\NotBlank(
            [
                'message' => 'mautic.user.user.password.notblank',
                'groups'  => ['CheckPasswordNotBlank'],
            ]
        ));

        $metadata->addPropertyConstraint('plainPassword', new Assert\Length(
            [
                'min'        => 6,
                'minMessage' => 'mautic.user.user.password.minlength',
                'groups'     => ['CheckPassword'],
            ]
        ));

        $metadata->addPropertyConstraint('plainPassword', new NotWeak(
            [
                'message'    => 'mautic.user.user.password.weak',
                'groups'     => ['CheckPassword'],
            ]
        ));

        $metadata->setGroupSequence(['User', 'SecondPass', 'CheckPassword']);
    }

    public static function determineValidationGroups(Form $form): array
    {
        $data   = $form->getData();
        $groups = ['User', 'SecondPass'];
        if ($data instanceof User) {
            $isNewUser        = !$data->getId();
            $hasPlainPassword = !empty($data->getPlainPassword());

            if ($isNewUser) {
                $groups[] = $hasPlainPassword ? 'CheckPassword' : 'CheckPasswordNotBlank';
            } elseif ($hasPlainPassword) {
                $groups[] = 'CheckPassword';
            }
        }

        return $groups;
    }

    /**
     * Prepares the metadata for API usage.
     */
    public static function loadApiMetadata(ApiMetadataDriver $metadata): void
    {
        $metadata->setGroupPrefix('user')
            ->addListProperties(
                [
                    'id',
                    'username',
                    'firstName',
                    'lastName',
                ]
            )
            ->addProperties(
                [
                    'email',
                    'position',
                    'role',
                    'timezone',
                    'locale',
                    'lastLogin',
                    'lastActive',
                    'signature',
                ]
            )
            ->build();
    }

    protected function isChanged($prop, $val)
    {
        $getter  = 'get'.ucfirst($prop);
        $current = $this->$getter();
        if ('role' == $prop) {
            if ($current && !$val) {
                $this->changes['role'] = [$current->getName().' ('.$current->getId().')', $val];
            } elseif (!$this->role && $val) {
                $this->changes['role'] = [$current, $val->getName().' ('.$val->getId().')'];
            } elseif ($current && $val && $current->getId() != $val->getId()) {
                $this->changes['role'] = [
                    $current->getName().'('.$current->getId().')',
                    $val->getName().'('.$val->getId().')',
                ];
            }
        } else {
            parent::isChanged($prop, $val);
        }
    }

    public function getUsername(): ?string
    {
        return $this->username;
    }

    public function getUserIdentifier(): string
    {
        return $this->username ?? '';
    }

    public function getSalt(): ?string
    {
        // bcrypt generates its own salt
        return null;
    }

    public function getPassword(): ?string
    {
        return $this->password;
    }

    /**
     * Get plain password.
     *
     * @return ?string
     */
    public function getPlainPassword()
    {
        return $this->plainPassword;
    }

    /**
     * Get current password (that a user has typed into a form).
     *
     * @return ?string
     */
    public function getCurrentPassword()
    {
        return $this->currentPassword;
    }

    public function getRoles(): array
    {
        $roles = [];

        if ($this->username) {
            $roles = [
                ($this->isAdmin()) ? 'ROLE_ADMIN' : 'ROLE_USER',
            ];

            if (defined('MAUTIC_API_REQUEST') && MAUTIC_API_REQUEST) {
                $roles[] = 'ROLE_API';
            }
        }

        return $roles;
    }

    #[\Deprecated]
    public function eraseCredentials(): void
    {
    }

    /**
     * @return array<int, mixed>
     */
    public function __serialize(): array
    {
        $this->plainPassword   = null;
        $this->currentPassword = null;

        return [
            $this->id,
            $this->username,
            $this->password,
            $this->isPublished(),
        ];
    }

    /**
     * @param array<int, mixed> $data
     */
    public function __unserialize(array $data): void
    {
        [
            $this->id,
            $this->username,
            $this->password,
            $published,
        ] = $data;
        $this->setIsPublished($published);
    }

    /**
     * @return ?int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set username.
     *
     * @param string $username
     *
     * @return User
     */
    public function setUsername($username)
    {
        $this->isChanged('username', $username);
        $this->username = $username;

        return $this;
    }

    /**
     * Set password.
     *
     * @param string $password
     *
     * @return User
     */
    public function setPassword($password)
    {
        $this->password = $password;

        return $this;
    }

    /**
     * Set plain password.
     *
     * @return User
     */
    public function setPlainPassword($plainPassword)
    {
        $this->plainPassword = $plainPassword;

        return $this;
    }

    /**
     * Set current password.
     *
     * @return User
     */
    public function setCurrentPassword($currentPassword)
    {
        $this->currentPassword = $currentPassword;

        return $this;
    }

    /**
     * Set firstName.
     *
     * @param string $firstName
     *
     * @return User
     */
    public function setFirstName($firstName)
    {
        $this->isChanged('firstName', $firstName);
        $this->firstName = $firstName;

        return $this;
    }

    /**
     * Get firstName.
     *
     * @return string
     */
    public function getFirstName()
    {
        return $this->firstName;
    }

    /**
     * Set lastName.
     *
     * @param string $lastName
     *
     * @return User
     */
    public function setLastName($lastName)
    {
        $this->isChanged('lastName', $lastName);
        $this->lastName = $lastName;

        return $this;
    }

    /**
     * Get lastName.
     *
     * @return string
     */
    public function getLastName()
    {
        return $this->lastName;
    }

    /**
     * Get full name.
     *
     * @param bool $lastFirst
     */
    public function getName($lastFirst = false): string
    {
        return ($lastFirst) ? $this->lastName.', '.$this->firstName : $this->firstName.' '.$this->lastName;
    }

    /**
     * Set email.
     *
     * @param string $email
     *
     * @return User
     */
    public function setEmail($email)
    {
        $this->isChanged('email', $email);
        $this->email = $email;

        return $this;
    }

    /**
     * Get email.
     *
     * @return string
     */
    public function getEmail()
    {
        return $this->email;
    }

    /**
     * Set role.
     *
     * @return User
     */
    public function setRole(?Role $role = null)
    {
        $this->isChanged('role', $role);
        $this->role = $role;

        return $this;
    }

    /**
     * Get role.
     *
     * @return Role
     */
    public function getRole()
    {
        return $this->role;
    }

    /**
     * Set active permissions.
     *
     * @return User
     */
    public function setActivePermissions(array $permissions)
    {
        $this->activePermissions = $permissions;

        return $this;
    }

    /**
     * Get active permissions.
     *
     * @return mixed
     */
    public function getActivePermissions()
    {
        return $this->activePermissions;
    }

    /**
     * Set position.
     *
     * @param string $position
     *
     * @return User
     */
    public function setPosition($position)
    {
        $this->isChanged('position', $position);
        $this->position = $position;

        return $this;
    }

    /**
     * Get position.
     *
     * @return string
     */
    public function getPosition()
    {
        return $this->position;
    }

    /**
     * Set timezone.
     *
     * @param string $timezone
     *
     * @return User
     */
    public function setTimezone($timezone)
    {
        $this->isChanged('timezone', $timezone);
        $this->timezone = $timezone;

        return $this;
    }

    /**
     * Get timezone.
     *
     * @return string
     */
    public function getTimezone()
    {
        return $this->timezone;
    }

    /**
     * @return User
     */
    public function setLocale(?string $locale)
    {
        $this->isChanged('locale', $locale);
        $this->locale = $locale;

        return $this;
    }

    /**
     * Get locale.
     *
     * @return string
     */
    public function getLocale()
    {
        return $this->locale;
    }

    /**
     * Determines if user is admin.
     *
     * @return bool
     */
    public function isAdmin()
    {
        if (null !== $this->role) {
            return $this->role->isAdmin();
        } else {
            return false;
        }
    }

    /**
     * @return mixed
     */
    public function getLastLogin()
    {
        return $this->lastLogin;
    }

    /**
     * @param mixed $lastLogin
     */
    public function setLastLogin($lastLogin = null): void
    {
        if (empty($lastLogin)) {
            $lastLogin = new \DateTime();
        }
        $this->lastLogin = $lastLogin;
    }

    /**
     * @return mixed
     */
    public function getLastActive()
    {
        return $this->lastActive;
    }

    /**
     * @param mixed $lastActive
     */
    public function setLastActive($lastActive = null): void
    {
        if (empty($lastActive)) {
            $lastActive = new \DateTime();
        }
        $this->lastActive = $lastActive;
    }

    /**
     * @return mixed
     */
    public function getPreferences()
    {
        return $this->preferences;
    }

    /**
     * @param mixed $preferences
     */
    public function setPreferences(array $preferences): void
    {
        $this->preferences = $preferences;
    }

    /**
     * Set signature.
     *
     * @param string $signature
     *
     * @return User
     */
    public function setSignature($signature)
    {
        $this->isChanged('signature', $signature);
        $this->signature = $signature;

        return $this;
    }

    /**
     * Get signature.
     *
     * @return string
     */
    public function getSignature()
    {
        return $this->signature;
    }

    /**
     * Needed for SAML to work correctly.
     */
    public function isEqualTo(UserInterface $user): bool
    {
        if (!$user instanceof self) {
            return false;
        }

        $thisUser = $this->getId().$this->getUserIdentifier().$this->getPassword();
        $thatUser = $user->getId().$user->getUserIdentifier().$user->getPassword();

        return $thisUser === $thatUser;
    }

    /**
     * @return bool
     */
    public function isGuest()
    {
        return $this->guest;
    }

    public function getCacheNamespacesToDelete(): array
    {
        return [self::CACHE_NAMESPACE];
    }
}
