<?php

declare(strict_types=1);

/*
 * This file is a part of the DiscordPHP project.
 *
 * Copyright (c) 2015-present David Cole <david.cole1340@gmail.com>
 *
 * This file is subject to the MIT license that is bundled
 * with this source code in the LICENSE.md file.
 */

namespace Discord\Parts\User;

use Carbon\Carbon;
use Discord\Builders\MessageBuilder;
use Discord\Helpers\BigInt;
use Discord\Helpers\Collection;
use Discord\Helpers\CollectionInterface;
use Discord\Helpers\ExCollectionInterface;
use Discord\Http\Endpoint;
use Discord\Http\Exceptions\NoPermissionsException;
use Discord\Parts\Channel\Channel;
use Discord\Parts\Channel\Message;
use Discord\Parts\Channel\Message\AllowedMentions;
use Discord\Parts\Channel\Overwrite;
use Discord\Parts\Guild\Ban;
use Discord\Parts\Guild\Guild;
use Discord\Parts\Guild\Role;
use Discord\Parts\Part;
use Discord\Parts\Permissions\Permission;
use Discord\Parts\Permissions\RolePermission;
use Discord\Parts\Thread\Thread;
use Discord\Parts\WebSockets\PresenceUpdate;
use React\Promise\PromiseInterface;
use Stringable;

use function React\Promise\reject;

/**
 * A member is a relationship between a user and a guild. It contains user-to-guild specific data like roles.
 *
 * @link https://discord.com/developers/docs/resources/guild#guild-member-object
 *
 * @since 2.0.0
 *
 * @property      ?User|null                         $user                         The user part of the member.
 * @property-read string|null                        $username                     The username of the member.
 * @property      ?string|null                       $nick                         The nickname of the member.
 * @property-read string                             $displayname                  The nickname or display name with optional discriminator of the member.
 * @property      ?string|null                       $avatar                       The avatar URL of the member or null if member has no guild avatar.
 * @property-read string|null                        $avatar_hash                  The avatar hash of the member or null if member has no guild avatar.
 * @property      ?string|null                       $banner                       The guild banner's URL of the member or null if the member has no guild banner.
 * @property-read string|null                        $banner_hash                  The guild banner hash of the member or null if the member has no guild banner.
 * @property      ExCollectionInterface<Role>|Role[] $roles                        A collection of Roles that the member has.
 * @property      ?Carbon|null                       $joined_at                    A timestamp of when the member joined the guild.
 * @property      ?Carbon|null                       $premium_since                When the user started boosting the server.
 * @property      bool                               $deaf                         Whether the member is deaf.
 * @property      bool                               $mute                         Whether the member is mute.
 * @property      int                                $flags                        Guild member flags represented as a bit set, defaults to `0`.
 * @property      bool|null                          $pending                      Whether the user has not yet passed the guild's Membership Screening requirements.
 * @property      RolePermission|null                $permissions                  Total permissions of the member in the channel, including overwrites, returned when in the interaction object.
 * @property      ?Carbon|null                       $communication_disabled_until When the user's timeout will expire and the user will be able to communicate in the guild again, null or a time in the past if the user is not timed out.
 * @property-read string|null                        $avatar_decoration            The member's guild avatar decoration URL.
 * @property-read string|null                        $avatar_decoration_hash       The member's guild avatar decoration hash.
 * @property      ?AvatarDecorationData|null         $avatar_decoration_data       Data for the member's guild avatar decoration.
 *
 * @property      string|null $guild_id The unique identifier of the guild that the member belongs to.
 * @property-read Guild|null  $guild    The guild that the member belongs to.
 *
 * @property      string                                     $id            The unique identifier of the member.
 * @property      string                                     $status        The status of the member.
 * @property-read Activity                                   $game          The game the member is playing.
 * @property      ExCollectionInterface<Activity>|Activity[] $activities    User's current activities.
 * @property      ClientStatus                               $client_status Current client status.
 *
 * @method PromiseInterface<Message> sendMessage(MessageBuilder $builder)
 */
class Member extends Part implements Stringable
{
    /** Member has left and rejoined the guild. */
    public const FLAGS_DID_REJOIN = (1 << 0);
    /** Member has completed onboarding. */
    public const FLAGS_COMPLETED_ONBOARDING = (1 << 1);
    /** Member is exempt from guild verification requirements. */
    public const FLAGS_BYPASSES_VERIFICATION = (1 << 2);
    /** Member has started onboarding, */
    public const FLAGS_STARTED_ONBOARDING = (1 << 3);
    /** Member is a guest and can only access the voice channel they were invited to. */
    public const FLAGS_IS_GUEST = (1 << 4);
    /** Member has started Server Guide new member actions. */
    public const FLAGS_STARTED_HOME_ACTIONS = (1 << 5);
    /** Member has completed Server Guide new member actions. */
    public const FLAGS_COMPLETED_HOME_ACTIONS = (1 << 6);
    /** Member's username, display name, or nickname is blocked by AutoMod. */
    public const FLAGS_AUTOMOD_QUARANTINED_USERNAME = (1 << 7);
    /** Member has dismissed the DM settings upsell. */
    public const FLAGS_DM_SETTINGS_UPSELL_ACKNOWLEDGED = (1 << 9);
    /** Member's guild tag is blocked by AutoMod. */
    public const FLAGS_AUTOMOD_QUARANTINED_GUILD_TAG = (1 << 10);

    /**
     * @inheritDoc
     */
    protected $fillable = [
        'user',
        'nick',
        'avatar',
        'banner',
        'roles',
        'joined_at',
        'premium_since',
        'deaf',
        'mute',
        'flags',
        'pending',
        'permissions',
        'communication_disabled_until',
        'avatar_decoration_data',

        // partial
        'guild_id',

        // @internal
        'id',
        'status',
        'activities',
        'client_status',
    ];

    /**
     * Updates the member from a new presence update object.
     * This is an internal function and is not meant to be used by a public application.
     *
     * @internal
     *
     * @param PresenceUpdate $presence
     *
     * @return PresenceUpdate Old presence.
     */
    public function updateFromPresence(PresenceUpdate $presence): PresenceUpdate
    {
        $rawPresence = $presence->getRawAttributes();
        $oldPresence = $this->factory->part(PresenceUpdate::class, $this->attributes, true);

        $this->attributes = array_merge($this->attributes, $rawPresence);

        return $oldPresence;
    }

    /**
     * Bans the member. Alias for `$guild->bans->ban()`.
     *
     * @see BanRepository::ban()
     *
     * @param int|null    $daysToDeleteMessages The amount of days to delete messages from.
     * @param string|null $reason               Reason of the Ban.
     *
     * @throws \RuntimeException      Member has no `$guild`.
     * @throws NoPermissionsException Missing `ban_members` permission.
     *
     * @return PromiseInterface<Ban>
     */
    public function ban(?int $daysToDeleteMessages = null, ?string $reason = null): PromiseInterface
    {
        return $this->discord->guilds->cacheGet($this->guild_id)->then(function (?Guild $guild) use ($daysToDeleteMessages, $reason) {
            if (null === $guild) {
                return reject(new \RuntimeException('Member has no Guild Part'));
            }

            if ($botperms = $guild->getBotPermissions()) {
                if (! $botperms->ban_members) {
                    return reject(new NoPermissionsException("You do not have permission to ban members in the guild {$guild->id}."));
                }
            }

            return $guild->bans->ban($this, ['delete_message_days' => $daysToDeleteMessages], $reason);
        });
    }

    /**
     * Alias for `$guild->members->delete()`.
     *
     * @param string|null $reason Reason for Audit Log.
     *
     * @throws \RuntimeException      Member has no `$guild`.
     * @throws NoPermissionsException Missing `kick_members` permission.
     *
     * @return PromiseInterface<self>
     */
    public function kick(?string $reason = null): PromiseInterface
    {
        return $this->discord->guilds->cacheGet($this->guild_id)->then(function (?Guild $guild) use ($reason) {
            if (null === $guild) {
                return new \RuntimeException('Member has no Guild Part');
            }

            if ($botperms = $guild->getBotPermissions()) {
                if (! $botperms->kick_members) {
                    return reject(new NoPermissionsException("You do not have permission to kick members in the guild {$guild->id}."));
                }
            }

            return $guild->members->delete($this, $reason);
        });
    }

    /**
     * Sets the nickname of the member.
     *
     * @param ?string|null $nick   The nickname of the member.
     * @param string|null  $reason Reason for Audit Log.
     *
     * @throws NoPermissionsException Missing manage_nicknames permission.
     *
     * @return PromiseInterface<self>
     */
    public function setNickname(?string $nick = null, ?string $reason = null): PromiseInterface
    {
        $payload = [
            'nick' => $nick ?? '',
        ];

        $headers = [];
        if (isset($reason)) {
            $headers['X-Audit-Log-Reason'] = $reason;
        }

        // jake plz
        if ($this->discord->id === $this->id) {
            $this->discord->getLogger()->warning('Modifying the bot member nickname via Member::setNickname is deprecated. Use MemberRepository::modifyCurrentMember instead.');

            return $this->http->patch(Endpoint::bind(Endpoint::GUILD_MEMBER_SELF, $this->guild_id), $payload, $headers);
        }

        if ($guild = $this->guild) {
            if ($botperms = $guild->getBotPermissions()) {
                if (! $botperms->manage_nicknames) {
                    return reject(new NoPermissionsException("You do not have permission to manage nicknames in the guild {$guild->id}."));
                }
            }
        }

        return $this->http->patch(Endpoint::bind(Endpoint::GUILD_MEMBER, $this->guild_id, $this->id), $payload, $headers)
            ->then(function ($response) {
                $this->nick = $response->nick;

                return $this;
            });
    }

    /**
     * Moves the member to another voice channel.
     *
     * @param Channel|string $channel The channel to move the member to.
     * @param string|null    $reason  Reason for Audit Log.
     *
     * @return PromiseInterface<self>
     */
    public function moveMember($channel, ?string $reason = null): PromiseInterface
    {
        if ($channel instanceof Channel) {
            $channel = $channel->id;
        }

        $headers = [];
        if (isset($reason)) {
            $headers['X-Audit-Log-Reason'] = $reason;
        }

        return $this->http->patch(Endpoint::bind(Endpoint::GUILD_MEMBER, $this->guild_id, $this->id), ['channel_id' => $channel], $headers)
            ->then(fn ($response) => $this);
    }

    /**
     * Adds a role to the member.
     *
     * @link https://discord.com/developers/docs/resources/guild#add-guild-member-role
     *
     * @param Role|string $role   The role to add to the member.
     * @param string|null $reason Reason for Audit Log.
     *
     * @throws \RuntimeException
     * @throws NoPermissionsException Missing manage_roles permission.
     *
     * @return PromiseInterface
     */
    public function addRole($role, ?string $reason = null): PromiseInterface
    {
        if ($role instanceof Role) {
            $role = $role->id;
        }

        // We don't want a double up on roles
        if (in_array($role, (array) $this->attributes['roles'])) {
            return reject(new \RuntimeException('Member already has role.'));
        }

        if ($guild = $this->guild) {
            if ($botperms = $guild->getBotPermissions()) {
                if (! $botperms->manage_roles) {
                    return reject(new NoPermissionsException("You do not have permission to add member role in the guild {$guild->id}."));
                }
            }
            if ($botHighestRole = $guild->roles->getCurrentMemberHighestRole()) {
                if ($botHighestRole->comparePosition($this) <= 0) {
                    return reject(new NoPermissionsException("The bot's highest role is not higher than the role {$this->id} in guild {$this->guild_id}."));
                }
            }
        }

        $headers = [];
        if (isset($reason)) {
            $headers['X-Audit-Log-Reason'] = $reason;
        }

        return $this->http->put(Endpoint::bind(Endpoint::GUILD_MEMBER_ROLE, $this->guild_id, $this->id, $role), null, $headers)
            ->then(function () use ($role) {
                if (! in_array($role, $this->attributes['roles'])) {
                    $this->attributes['roles'][] = $role;
                }
            });
    }

    /**
     * Removes a role from the member.
     *
     * @link https://discord.com/developers/docs/resources/guild#remove-guild-member-role
     *
     * @param Role|string $role   The role to remove from the member.
     * @param string|null $reason Reason for Audit Log.
     *
     * @throws NoPermissionsException Missing manage_roles permission.
     *
     * @return PromiseInterface
     */
    public function removeRole($role, ?string $reason = null): PromiseInterface
    {
        if ($role instanceof Role) {
            $role = $role->id;
        }

        if ($guild = $this->guild) {
            if ($botperms = $guild->getBotPermissions()) {
                if (! $botperms->manage_roles) {
                    return reject(new NoPermissionsException("You do not have permission to remove member role in the guild {$guild->id}."));
                }
            }
            if ($botHighestRole = $guild->roles->getCurrentMemberHighestRole()) {
                if ($botHighestRole->comparePosition($this) <= 0) {
                    return reject(new NoPermissionsException("The bot's highest role is not higher than the role {$this->id} in guild {$this->guild_id}."));
                }
            }
        }

        $headers = [];
        if (isset($reason)) {
            $headers['X-Audit-Log-Reason'] = $reason;
        }

        return $this->http->delete(Endpoint::bind(Endpoint::GUILD_MEMBER_ROLE, $this->guild_id, $this->id, $role), null, $headers)
            ->then(function () use ($role) {
                if (($removeRole = array_search($role, $this->attributes['roles'])) !== false) {
                    unset($this->attributes['roles'][$removeRole]);
                }
            });
    }

    /**
     * Updates member roles.
     *
     * @link https://discord.com/developers/docs/resources/guild#modify-guild-member
     *
     * @param ExCollectionInterface<Role|string>|Role[]|string[] $roles  The roles to set to the member.
     * @param string|null                                        $reason Reason for Audit Log.
     *
     * @throws NoPermissionsException Missing manage_roles permission.
     *
     * @return PromiseInterface<self>
     */
    public function setRoles($roles, ?string $reason = null): PromiseInterface
    {
        if ($roles instanceof CollectionInterface) {
            $roles = $roles->toArray();
        }
        if (! is_array($roles)) {
            return reject(new \InvalidArgumentException('Roles must be an array of Role instances or Role IDs.'));
        }

        foreach ($roles as $i => $role) {
            if ($role instanceof Role) {
                $roles[$i] = $role->id;
            }
        }

        if ($guild = $this->guild) {
            if ($botperms = $guild->getBotPermissions()) {
                if (! $botperms->manage_roles) {
                    return reject(new NoPermissionsException("You do not have permission to manage member roles in the guild {$guild->id}."));
                }
            }
        }

        $headers = [];
        if (isset($reason)) {
            $headers['X-Audit-Log-Reason'] = $reason;
        }

        return $this->http->patch(Endpoint::bind(Endpoint::GUILD_MEMBER, $this->guild_id, $this->id), ['roles' => $roles], $headers)
            ->then(function ($response) {
                $this->attributes['roles'] = $response->roles;

                return $this;
            });
    }

    /**
     * Sends a message to the member.
     *
     * Takes a `MessageBuilder` or content of the message for the first parameter. If the first parameter
     * is an instance of `MessageBuilder`, the rest of the arguments are disregarded.
     *
     * @see User::sendMessage()
     *
     * @param MessageBuilder|string                 $message          The message builder that should be converted into a message, or the string content of the message.
     * @param bool                                  $tts              Whether the message is TTS.
     * @param \Discord\Parts\Embed\Embed|array|null $embed            An embed object or array to send in the message.
     * @param AllowedMentions|array|null            $allowed_mentions Allowed mentions object for the message.
     * @param Message|null                          $replyTo          Sends the message as a reply to the given message instance.
     *
     * @throws \RuntimeException
     *
     * @return PromiseInterface<Message>
     */
    public function sendMessage($message, bool $tts = false, $embed = null, $allowed_mentions = null, ?Message $replyTo = null): PromiseInterface
    {
        if ($user = $this->user) {
            return $user->sendMessage($message, $tts, $embed, $allowed_mentions, $replyTo);
        }

        return reject(new \RuntimeException('Member had no user part.'));
    }

    /**
     * Gets the total permissions of the member.
     *
     * Note that Discord permissions are complex and YOU need to account for the
     * fact that you cannot edit a role higher than your own.
     *
     * @link https://discord.com/developers/docs/topics/permissions
     *
     * @param Channel|Thread|null $channel The channel to check its permission overwrites. `null` for just Role.
     *
     * @throws \InvalidArgumentException
     *
     * @return RolePermission|null `null` if permission is failed to be determined.
     */
    public function getPermissions($channel = null): ?RolePermission
    {
        if (! $guild = $this->guild) {
            return null;
        }

        if ($channel) {
            if ($channel instanceof Thread) {
                $channel = $channel->parent;
            } elseif (! ($channel instanceof Channel)) {
                throw new \InvalidArgumentException('$channel must be an instance of Channel, Thread or null.');
            }
        }

        // Get @everyone role guild permission
        if (! $everyoneRole = $guild->roles->get('id', $guild->id)) {
            return null;
        }
        $bitwise = $everyoneRole->permissions->bitwise;

        // Prepare array for role ids
        $roles = [];

        // If this member is the guild owner
        if ($guild->owner_id === $this->id) {
            // Add administrator permission
            $bitwise = BigInt::set($bitwise, Permission::ROLE_PERMISSIONS['administrator']);
        } else {
            // Iterate all base roles
            /** @var Role */
            foreach ($this->roles as $id => $role) {
                // Remember the role id for later use
                $roles[] = $id;
                // Store permission value from this role
                $bitwise = BigInt::or($bitwise, $role->permissions->bitwise);
            }
        }

        // Create from computed base permissions
        /** @var RolePermission */
        $newPermission = $guild->createOf(RolePermission::class, ['bitwise' => $bitwise]);

        // If computed roles has Administrator permission
        if ($newPermission->administrator) {
            // Iterate all permissions of the computed roles
            foreach (RolePermission::getPermissions() as $permission => $_) {
                // Set permission to true
                $newPermission->{$permission} = true;
            }

            // Administrators ends here with all permissions
            return $newPermission;
        }

        // If channel is specified (overwrites)
        if ($channel) {
            // Get @everyone role channel permission
            /** @var Overwrite */
            if ($overwrite = $channel->overwrites->get('id', $guild->id)) {
                // Set "DENY" overwrites
                $bitwise = BigInt::and($bitwise, BigInt::not($overwrite->deny->bitwise));
                // Set "ALLOW" overwrites
                $bitwise = BigInt::or($bitwise, $overwrite->allow->bitwise);
            }

            // Prepare Allow and Deny buffers for role overwrite
            $allow = $deny = 0;

            // Iterate all roles channel permission
            /** @var Overwrite */
            foreach ($channel->overwrites as $overwrite) {
                // Check for Role overwrite or invalid roles
                if ($overwrite->type !== Overwrite::TYPE_ROLE || ! in_array($overwrite->id, $roles)) {
                    // Skip
                    continue;
                }

                // Get "ALLOW" permissions
                $allow = BigInt::or($allow, $overwrite->allow->bitwise);
                // Get "DENY" permissions
                $deny = BigInt::or($deny, $overwrite->deny->bitwise);
            }

            // Set role "DENY" permissions overwrite
            $bitwise = BigInt::and($bitwise, BigInt::not($deny));
            // Set role "ALLOW" permissions overwrite
            $bitwise = BigInt::or($bitwise, $allow);

            // Get this member specific overwrite
            /** @var Overwrite */
            if ($overwrite = $channel->overwrites->get('id', $this->id)) {
                // Set member "DENY" permissions overwrite
                $bitwise = BigInt::and($bitwise, BigInt::not($overwrite->deny->bitwise));
                // Set member "ALLOW" permissions overwrite
                $bitwise = BigInt::or($bitwise, $overwrite->allow->bitwise);
            }
        }

        // Re-create the Role Permissions from the computed overwrites
        return $guild->createOf(RolePermission::class, ['bitwise' => $bitwise]);
    }

    /**
     * Sets timeout on a member.
     *
     * @param Carbon|null $communication_disabled_until When the user's timeout will expire and the user will be able to communicate in the guild again, null or a time in the past if the user is not timed out.
     * @param string|null $reason                       Reason for Audit Log.
     *
     * @throws NoPermissionsException Missing moderate_members permission.
     *
     * @return PromiseInterface<self>
     */
    public function timeoutMember(?Carbon $communication_disabled_until, ?string $reason = null): PromiseInterface
    {
        if ($guild = $this->guild) {
            if ($botperms = $guild->getBotPermissions()) {
                if (! $botperms->moderate_members) {
                    return reject(new NoPermissionsException("You do not have permission to time out members in the guild {$guild->id}."));
                }
            }
        }

        $headers = [];
        if (isset($reason)) {
            $headers['X-Audit-Log-Reason'] = $reason;
        }

        return $this->http->patch(Endpoint::bind(Endpoint::GUILD_MEMBER, $this->guild_id, $this->id), ['communication_disabled_until' => $communication_disabled_until?->toIso8601ZuluString()], $headers)
            ->then(function ($response) {
                $this->attributes['communication_disabled_until'] = $response->communication_disabled_until;

                return $this;
            });
    }

    /**
     * Sets verification bypasses flag on a member.
     *
     * @param bool        $bypasses_verification Whether member is exempt from guild verification requirements.
     * @param string|null $reason                Reason for Audit Log.
     *
     * @throws NoPermissionsException Missing `moderate_members` permission.
     *
     * @return PromiseInterface<self>
     */
    public function setBypassesVerification(bool $bypasses_verification, ?string $reason = null): PromiseInterface
    {
        if ($guild = $this->guild) {
            if ($botperms = $guild->getBotPermissions()) {
                if (! $botperms->moderate_members) {
                    return reject(new NoPermissionsException("You do not have permission to modify member flag in the guild {$guild->id}."));
                }
            }
        }

        $headers = [];
        if (isset($reason)) {
            $headers['X-Audit-Log-Reason'] = $reason;
        }

        $flags = $this->flags;
        if ($bypasses_verification) {
            $flags |= self::FLAGS_BYPASSES_VERIFICATION;
        } else {
            $flags &= ~self::FLAGS_BYPASSES_VERIFICATION;
        }

        return $this->http->patch(Endpoint::bind(Endpoint::GUILD_MEMBER, $this->guild_id, $this->id), ['flags' => $flags], $headers)
            ->then(function ($response) {
                $this->attributes['flags'] = $response->flags;

                return $this;
            });
    }

    /**
     * Returns the member nickname or display name with optional #discriminator.
     *
     * @return string Either nick or global_name or username with optional #discriminator.
     */
    protected function getDisplaynameAttribute(): string
    {
        $user = $this->user;

        return ($this->nick ?? $user->global_name ?? $user->username).($user->discriminator ? '#'.$user->discriminator : '');
    }

    /**
     * Gets the game attribute.
     * Polyfill for the first activity.
     *
     * @return ?Activity
     */
    protected function getGameAttribute(): ?Activity
    {
        return $this->activities->get('type', Activity::TYPE_PLAYING);
    }

    /**
     * Gets the activities attribute.
     *
     * @return ExCollectionInterface<Activity>|Activity[]
     */
    protected function getActivitiesAttribute(): ExCollectionInterface
    {
        return $this->attributeCollectionHelper('activities', Activity::class);
    }

    /**
     * Gets the client status attribute.
     *
     * @return ClientStatus
     */
    protected function getClientStatusAttribute(): ClientStatus
    {
        if (! isset($this->attributes['client_status'])) {
            return $this->factory->part(ClientStatus::class, [], true);
        }

        return $this->attributePartHelper('client_status', ClientStatus::class);
    }

    /**
     * Returns the id attribute.
     *
     * @return string The user ID of the member.
     */
    protected function getIdAttribute(): string
    {
        if (isset($this->attributes['id'])) {
            return $this->attributes['id'];
        }

        if ($this->user) {
            $this->attributes['id'] = $this->user->id;

            return $this->user->id;
        }

        // The field `user` won't be included in the member object attached to MESSAGE_CREATE and MESSAGE_UPDATE gateway events.
        return '';
    }

    /**
     * Returns the username attribute.
     *
     * @return string|null The username of the member.
     */
    protected function getUsernameAttribute(): ?string
    {
        return $this->user->username ?? null;
    }

    /**
     * Returns the discriminator attribute.
     *
     * @deprecated 10.0.0 Use `$member->user->discriminator`
     *
     * @return string|null The discriminator of the member.
     */
    protected function getDiscriminatorAttribute(): ?string
    {
        return $this->user->discriminator ?? null;
    }

    /**
     * Returns the user attribute.
     *
     * The field `user` won't be included in the member object attached to MESSAGE_CREATE and MESSAGE_UPDATE gateway events.
     *
     * @return User|null The user that owns the member.
     */
    protected function getUserAttribute(): ?User
    {
        if (isset($this->attributes['id'])) {
            if ($user = $this->discord->users->get('id', $this->attributes['id'])) {
                $this->attributes['user'] = $user;

                return $user;
            }
        }

        if (isset($this->attributes['user'])) {
            return (function () {
                if (! $this->attributes['user'] instanceof User) {
                    $this->attributes['user'] = $this->createOf(User::class, ((array) $this->attributes['user']));
                    $this->attributes['id'] = $this->attributes['user']->id;
                }

                return $this->attributes['user'];
            })();
        }

        return null;
    }

    /**
     * Returns the guild attribute.
     *
     * @return Guild|null The guild.
     */
    protected function getGuildAttribute(): ?Guild
    {
        return $this->discord->guilds->get('id', $this->guild_id);
    }

    /**
     * Returns the roles attribute.
     *
     * @return ExCollectionInterface<Role>|Role[] A collection of roles the member is in. null role only contains ID in the collection.
     */
    protected function getRolesAttribute(): ExCollectionInterface
    {
        $roles = new Collection();

        if (empty($this->attributes['roles'])) {
            return $roles;
        }

        $roles->fill(array_fill_keys($this->attributes['roles'], null));

        if ($guild = $this->guild) {
            foreach ($guild->roles as $id => $role) {
                if (in_array($id, $this->attributes['roles'])) {
                    $roles->pushItem($role);
                }
            }
        }

        return $roles;
    }

    /**
     * Returns the joined at attribute.
     *
     * @return Carbon|null The timestamp from when the member joined.
     *
     * @throws \Exception
     */
    protected function getJoinedAtAttribute(): ?Carbon
    {
        return $this->attributeCarbonHelper('joined_at');
    }

    /**
     * Returns the guild avatar URL for the member.
     *
     * @param string|null $format The image format.
     * @param int         $size   The size of the image.
     *
     * @return string|null The URL to the member avatar or null.
     */
    public function getAvatarAttribute(?string $format = null, int $size = 1024): ?string
    {
        if (! isset($this->attributes['avatar'])) {
            return null;
        }

        if (isset($format)) {
            static $allowed = ['png', 'jpg', 'jpeg', 'webp', 'gif'];
            $format = strtolower($format);
            if (! in_array($format, $allowed)) {
                $format = 'webp';
            }
        } elseif (strpos($this->attributes['avatar'], 'a_') === 0) {
            $format = 'gif';
        } else {
            $format = 'webp';
        }

        // Clamp size to allowed powers of two between 16 and 4096
        $size = max(16, min(4096, $size));
        $size = 2 ** (int) round(log($size, 2));

        return "https://cdn.discordapp.com/guilds/{$this->guild_id}/users/{$this->id}/avatars/{$this->attributes['avatar']}.{$format}?size={$size}";
    }

    /**
     * Returns the guild avatar hash for the member.
     *
     * @return string|null The member avatar's hash or null.
     */
    protected function getAvatarHashAttribute(): ?string
    {
        return $this->attributes['avatar'] ?? null;
    }

    /**
     * Returns the guild banner URL for the member.
     *
     * @param string|null $format The image format.
     * @param int         $size   The size of the image.
     *
     * @return string|null The URL to the member banner or null.
     */
    protected function getBannerAttribute(?string $format = null, int $size = 1024): ?string
    {
        if (! isset($this->attributes['banner'])) {
            return null;
        }

        if (isset($format)) {
            static $allowed = ['png', 'jpg', 'jpeg', 'webp', 'gif'];
            $format = strtolower($format);
            if (! in_array($format, $allowed)) {
                $format = 'webp';
            }
        } elseif (strpos($this->attributes['banner'], 'a_') === 0) {
            $format = 'gif';
        } else {
            $format = 'webp';
        }

        // Clamp size to allowed powers of two between 16 and 4096
        $size = max(16, min(4096, $size));
        $size = 2 ** (int) round(log($size, 2));

        return "https://cdn.discordapp.com/guilds/{$this->guild_id}/users/{$this->id}/banners/{$this->attributes['banner']}.{$format}?size={$size}";
    }

    /**
     * Returns the guild banner hash for the member.
     *
     * @return string|null The member's banner hash or null.
     */
    protected function getBannerHashAttribute(): ?string
    {
        return $this->attributes['banner'] ?? null;
    }

    /**
     * Returns the premium since attribute.
     *
     * @return Carbon|null
     *
     * @throws \Exception
     */
    protected function getPremiumSinceAttribute(): ?Carbon
    {
        return $this->attributeCarbonHelper('premium_since');
    }

    /**
     * Returns the permissions attribute.
     * This is only available from Interaction, use Member::getPermissions() for normal permissions.
     *
     * @see Member::getPermissions()
     *
     * @return RolePermission|null The total calculated permissions, only available from Interaction.
     */
    protected function getPermissionsAttribute(): ?RolePermission
    {
        if (! isset($this->attributes['permissions'])) {
            return null;
        }

        return $this->factory->part(RolePermission::class, ['bitwise' => $this->attributes['permissions']], true);
    }

    /**
     * Returns the communication disabled until attribute.
     *
     * @return Carbon|null
     *
     * @throws \Exception
     */
    protected function getCommunicationDisabledUntilAttribute(): ?Carbon
    {
        return $this->attributeCarbonHelper('communication_disabled_until');
    }

    /**
     * Returns the member's guild avatar decoration URL for the client.
     *
     * @param string|null $format The image format. (Only 'png' is allowed)
     * @param int         $size   The size of the image.
     *
     * @return string|null The URL to the clients avatar decoration.
     */
    public function getAvatarDecorationAttribute(?string $format = 'png', int $size = 288): ?string
    {
        if (! isset($this->attributes['avatar_decoration_data'])) {
            return null;
        }

        // Clamp size to allowed powers of two between 16 and 4096
        $size = max(16, min(4096, $size));
        $size = 2 ** (int) round(log($size, 2));

        if (! $asset = $this->avatar_decoration_data->asset ?? null) {
            return null;
        }

        return "https://cdn.discordapp.com/avatar-decoration-presets/{$asset}.{$format}?size={$size}";
    }

    /**
     * Returns the avatar decoration hash for the member.
     *
     * @return ?string The member's guild avatar decoration's hash.
     */
    public function getAvatarDecorationHashAttribute(): ?string
    {
        return $this->avatar_decoration_data->asset ?? null;
    }

    /**
     * Returns the avatar decoration data for the member.
     *
     * @return AvatarDecorationData|null
     */
    protected function getAvatarDecorationDataAttribute(): ?AvatarDecorationData
    {
        return $this->attributePartHelper('avatar_decoration_data', AvatarDecorationData::class);
    }

    /**
     * Returns the voicechannel of the member.
     *
     * @return Channel|null
     */
    public function getVoiceChannel(): ?Channel
    {
        if ($guild = $this->guild) {
            return $guild->channels->find(function (Channel $channel) {
                if ($channel->isVoiceBased() && $members = $channel->members) {
                    return $members->offsetExists($this->id);
                }

                return false;
            });
        }

        return null;
    }

    /**
     * @inheritDoc
     *
     * @link https://discord.com/developers/docs/resources/guild#modify-guild-member-json-params
     */
    public function getUpdatableAttributes(): array
    {
        return [
            'roles' => array_values($this->attributes['roles']),
        ];
    }

    /**
     * @inheritDoc
     */
    public function save(?string $reason = null): PromiseInterface
    {
        if (! isset($this->attributes['guild_id'])) {
            return parent::save();
        }

        /** @var Guild */
        $guild = $this->guild ?? $this->factory->part(Guild::class, ['id' => $this->attributes['guild_id']], true);

        if ($this->id === $this->discord->id) {
            $data = [];
            if ($this->nick) {
                $data['nick'] = $this->nick;
            }
            if ($this->banner_hash) {
                $data['banner'] = $this->banner_hash;
            }
            if ($this->avatar_hash) {
                $data['avatar'] = $this->avatar_hash;
            }
            if ($this->bio) {
                $data['bio'] = $this->bio;
            }

            return $guild->members->modifyCurrentMember($guild, $data, $reason);
        }

        // @todo Add more permission checks
        // @link https://discord.com/developers/docs/resources/guild#modify-guild-member

        return $guild->members->save($this, $reason);
    }

    /**
     * @inheritDoc
     */
    public function getRepositoryAttributes(): array
    {
        return [
            'user_id' => $this->id,
        ];
    }

    /**
     * Returns a formatted mention.
     *
     * @return string
     */
    public function __toString(): string
    {
        return "<@{$this->id}>";
    }
}
