<?php

/**
 * @package    Grav\Common\User
 *
 * @copyright  Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
 * @license    MIT License; see LICENSE file for details.
 */

namespace Grav\Common\User\Traits;

use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Common\Page\Medium\ImageMedium;
use Grav\Common\Page\Medium\Medium;
use Grav\Common\Page\Medium\StaticImageMedium;
use Grav\Common\User\Authentication;
use Grav\Common\Utils;
use Multiavatar;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use function is_array;
use function is_string;

/**
 * Trait UserTrait
 * @package Grav\Common\User\Traits
 */
trait UserTrait
{
    /**
     * Authenticate user.
     *
     * If user password needs to be updated, new information will be saved.
     *
     * @param string $password Plaintext password.
     * @return bool
     */
    public function authenticate(string $password): bool
    {
        $hash = $this->get('hashed_password');

        $isHashed = null !== $hash;
        if (!$isHashed) {
            // If there is no hashed password, fake verify with default hash.
            $hash = Grav::instance()['config']->get('system.security.default_hash');
        }

        // Always execute verify() to protect us from timing attacks, but make the test to fail if hashed password wasn't set.
        $result = Authentication::verify($password, $hash) && $isHashed;

        $plaintext_password = $this->get('password');
        if (null !== $plaintext_password) {
            // Plain-text password is still stored, check if it matches.
            if ($password !== $plaintext_password) {
                return false;
            }

            // Force hash update to get rid of plaintext password.
            $result = 2;
        }

        if ($result === 2) {
            // Password needs to be updated, save the user.
            $this->set('password', $password);
            $this->undef('hashed_password');
            $this->save();
        }

        return (bool)$result;
    }

    /**
     * Checks user authorization to the action.
     *
     * @param  string $action
     * @param  string|null $scope
     * @return bool|null
     */
    public function authorize(string $action, string $scope = null): ?bool
    {
        // User needs to be enabled.
        if ($this->get('state', 'enabled') !== 'enabled') {
            return false;
        }

        // User needs to be logged in.
        if (!$this->get('authenticated')) {
            return false;
        }

        // User needs to be authorized (2FA).
        if (strpos($action, 'login') === false && !$this->get('authorized', true)) {
            return false;
        }

        if (null !== $scope) {
            $action = $scope . '.' . $action;
        }

        $config = Grav::instance()['config'];
        $authorized = false;

        //Check group access level
        $groups = (array)$this->get('groups');
        foreach ($groups as $group) {
            $permission = $config->get("groups.{$group}.access.{$action}");
            $authorized = Utils::isPositive($permission);
            if ($authorized === true) {
                break;
            }
        }

        //Check user access level
        $access = $this->get('access');
        if ($access && Utils::getDotNotation($access, $action) !== null) {
            $permission = $this->get("access.{$action}");
            $authorized = Utils::isPositive($permission);
        }

        return $authorized;
    }

    /**
     * Return media object for the User's avatar.
     *
     * Note: if there's no local avatar image for the user, you should call getAvatarUrl() to get the external avatar URL.
     *
     * @return ImageMedium|StaticImageMedium|null
     */
    public function getAvatarImage(): ?Medium
    {
        $avatars = $this->get('avatar');
        if (is_array($avatars) && $avatars) {
            $avatar = array_shift($avatars);

            $media = $this->getMedia();
            $name = $avatar['name'] ?? null;

            $image = $name ? $media[$name] : null;
            if ($image instanceof ImageMedium ||
                $image instanceof StaticImageMedium) {
                return $image;
            }
        }

        return null;
    }

    /**
     * Return the User's avatar URL
     *
     * @return string
     */
    public function getAvatarUrl(): string
    {
        // Try to locate avatar image.
        $avatar = $this->getAvatarImage();
        if ($avatar) {
            return $avatar->url();
        }

        // Try if avatar is a sting (URL).
        $avatar = $this->get('avatar');
        if (is_string($avatar)) {
            return $avatar;
        }

        // Try looking for provider.
        $provider = $this->get('provider');
        $provider_options = $this->get($provider);
        if (is_array($provider_options)) {
            if (isset($provider_options['avatar_url']) && is_string($provider_options['avatar_url'])) {
                return $provider_options['avatar_url'];
            }
            if (isset($provider_options['avatar']) && is_string($provider_options['avatar'])) {
                return $provider_options['avatar'];
            }
        }

        $email = $this->get('email');
        $avatar_generator = Grav::instance()['config']->get('system.accounts.avatar', 'multiavatar');
        if ($avatar_generator === 'gravatar') {
            if (!$email) {
                return '';
            }

            $hash = md5(strtolower(trim($email)));

            return 'https://www.gravatar.com/avatar/' . $hash;
        }

        $hash = $this->get('avatar_hash');
        if (!$hash) {
            $username = $this->get('username');
            $hash = md5(strtolower(trim($email ?? $username)));
        }

        return $this->generateMultiavatar($hash);
    }

    /**
     * @param string $hash
     * @return string
     */
    protected function generateMultiavatar(string $hash): string
    {
        /** @var UniformResourceLocator $locator */
        $locator = Grav::instance()['locator'];

        $storage = $locator->findResource('image://multiavatar', true, true);
        $avatar_file = "{$storage}/{$hash}.svg";

        if (!file_exists($storage)) {
            Folder::create($storage);
        }

        if (!file_exists($avatar_file)) {
            $mavatar = new Multiavatar();

            file_put_contents($avatar_file, $mavatar->generate($hash, null, null));
        }

        $avatar_url = $locator->findResource("image://multiavatar/{$hash}.svg", false, true);

        return Utils::url($avatar_url);

    }

    abstract public function get($name, $default = null, $separator = null);
    abstract public function set($name, $value, $separator = null);
    abstract public function undef($name, $separator = null);
    abstract public function save();
}