<?php

declare(strict_types=1);

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

namespace Discord\Parts\Guild;

use Carbon\Carbon;
use Discord\Helpers\ExCollectionInterface;
use Discord\Http\Endpoint;
use Discord\Http\Exceptions\NoPermissionsException;
use Discord\Parts\Channel\Channel;
use Discord\Parts\Part;
use Discord\Parts\User\User;
use Discord\Repository\Guild\ScheduledEventRepository;
use React\Promise\PromiseInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

use function React\Promise\reject;

/**
 * A representation of a scheduled event in a guild.
 *
 * @link https://discord.com/developers/docs/resources/guild-scheduled-event
 *
 * @since 7.0.0
 *
 * @property      string                                                                   $id                               The id of the scheduled event.
 * @property      string                                                                   $guild_id                         The guild id which the scheduled event belongs to.
 * @property-read Guild|null                                                               $guild                            The guild which the scheduled event belongs to.
 * @property      ?string|null                                                             $channel_id                       The channel id in which the scheduled event will be hosted, or null if scheduled entity type is EXTERNAL.
 * @property-read Channel|null                                                             $channel                          The channel in which the scheduled event will be hosted, or null.
 * @property      ?string|null                                                             $creator_id                       The id of the user that created the scheduled event.
 * @property      string                                                                   $name                             The name of the scheduled event (1-100 characters).
 * @property      ?string|null                                                             $description                      The description of the scheduled event (1-1000 characters).
 * @property      Carbon                                                                   $scheduled_start_time             The time the scheduled event will start.
 * @property      Carbon|null                                                              $scheduled_end_time               The time the scheduled event will end, required if entity_type is EXTERNAL.
 * @property      int                                                                      $privacy_level                    The privacy level of the scheduled event.
 * @property      int                                                                      $status                           The status of the scheduled event.
 * @property      int                                                                      $entity_type                      The type of the scheduled event.
 * @property      ?string                                                                  $entity_id                        The id of an entity associated with a guild scheduled event.
 * @property      ?EntityMetadata                                                          $entity_metadata                  Additional metadata for the guild scheduled event.
 * @property      User|null                                                                $creator                          The user that created the scheduled event.
 * @property      int|null                                                                 $user_count                       The number of users subscribed to the scheduled event.
 * @property      ?string|null                                                             $image                            The cover image URL of the scheduled event.
 * @property-read string|null                                                              $image_hash                       The cover image hash of the scheduled event.
 * @property      RecurrenceRule|null                                                      $recurrence_rule                  The definition for how often this event should recur.
 * @property      ExCollectionInterface<ScheduledEventException>|ScheduledEventException[] $guild_scheduled_event_exceptions The exceptions to the recurrence rule of the guild scheduled event.
 */
class ScheduledEvent extends Part
{
    /** The scheduled event is only accessible to guild members. */
    public const PRIVACY_LEVEL_GUILD_ONLY = 2;

    public const ENTITY_TYPE_STAGE_INSTANCE = 1;
    public const ENTITY_TYPE_VOICE = 2;
    public const ENTITY_TYPE_EXTERNAL = 3;

    public const STATUS_SCHEDULED = 1;
    public const STATUS_ACTIVE = 2;
    public const STATUS_COMPLETED = 3;
    public const STATUS_CANCELED = 4;

    /**
     * @inheritDoc
     */
    protected $fillable = [
        'id',
        'guild_id',
        'channel_id',
        'creator_id',
        'name',
        'description',
        'image',
        'scheduled_start_time',
        'scheduled_end_time',
        'privacy_level',
        'status',
        'entity_type',
        'entity_id',
        'entity_metadata',
        'creator',
        'user_count',
    ];

    /**
     * Create an exception for the guild scheduled event's recurrence rule.
     * Returns the created guild scheduled event exception object on success.
     * Fires a Guild Scheduled Event Exception Create Gateway event.
     *
     * @param array   $options                         The scheduled event exception data.
     * @param ?Carbon $options['scheduled_start_time'] The new time at when the scheduled event recurrence will start, if applicable.
     * @param ?Carbon $options['scheduled_end_time']   The new time at when the scheduled event recurrence will end, if applicable.
     * @param ?bool   $options['is_canceled']          Whether or not the scheduled event will be skipped on the recurrence.
     * @param ?string $reason                          Reason for Audit Log.
     *
     * @return PromiseInterface<ScheduledEventException>
     */
    public function createException(array $options, ?string $reason = null): PromiseInterface
    {
        $mandatory = $payload = ['original_scheduled_start_time' => $this->scheduled_start_time];

        if (isset($options['scheduled_start_time'])) {
            $payload['scheduled_start_time'] = $options['scheduled_start_time'];
        }

        if (isset($options['scheduled_end_time'])) {
            $payload['scheduled_end_time'] = $options['scheduled_end_time'];
        }

        if (isset($options['is_canceled'])) {
            $payload['is_canceled'] = $options['is_canceled'];
        }

        if ($payload === $mandatory) {
            return reject(new \InvalidArgumentException('At minimum, you must provide a value for one of `is_canceled`, `scheduled_start_time`, or `scheduled_end_time`.'));
        }

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

        return $this->http->post(Endpoint::bind(Endpoint::GUILD_SCHEDULED_EVENT_EXCEPTIONS, $this->guild_id, $this->id), $payload, $headers)
            ->then(
                function ($response): ScheduledEventException {
                    $part = $this->factory->part(ScheduledEventException::class, (array) $response, true);

                    $this->guild_scheduled_event_exceptions->pushItem($part);

                    return $part;
                }
            );
    }

    /**
     * Get a list of guild scheduled event users subscribed to a guild scheduled
     * event.
     * Returns a list of guild scheduled event user objects on success.
     * Guild member data, if it exists, is included if the with_member query
     * parameter is set.
     *
     * @link https://discord.com/developers/docs/resources/guild-scheduled-event#get-guild-scheduled-event-users
     *
     * @param array $options
     *
     * @throws \RangeException
     *
     * @return PromiseInterface<ExCollectionInterface<User>>
     */
    public function getUsers(array $options): PromiseInterface
    {
        $resolver = new OptionsResolver();
        $resolver->setDefaults(['limit' => 100, 'with_member' => false]);
        $resolver->setDefined(['before', 'after']);
        $resolver->setAllowedTypes('before', [User::class, 'string']);
        $resolver->setAllowedTypes('after', [User::class, 'string']);
        $resolver->setAllowedTypes('with_member', 'bool');
        $resolver->setAllowedValues('limit', fn ($value) => ($value >= 1 && $value <= 100));

        $options = $resolver->resolve($options);
        if (isset($options['before'], $options['after'])) {
            return reject(new \RangeException('Can only specify one of before after.'));
        }

        $endpoint = Endpoint::bind(Endpoint::GUILD_SCHEDULED_EVENT_USERS, $this->guild_id, $this->id);
        $endpoint->addQuery('limit', $options['limit']);
        $endpoint->addQuery('with_member', $options['with_member']);

        if (isset($options['before'])) {
            $endpoint->addQuery('before', $options['before'] instanceof User ? $options['before']->id : $options['before']);
        }
        if (isset($options['after'])) {
            $endpoint->addQuery('after', $options['after'] instanceof User ? $options['after']->id : $options['after']);
        }

        return $this->http->get($endpoint)->then(function ($responses) {
            /** @var ExCollectionInterface $users */
            $users = new ($this->discord->getCollectionClass());

            $guild = $this->guild;

            foreach ($responses as $response) {
                if (isset($response->member) && ! $user = $guild->members->get('id', $response->user->id)) {
                    $user = $guild->members->create($response->member, true);
                    $guild->members->pushItem($user);
                } elseif (! $user = $this->discord->users->get('id', $response->user->id)) {
                    $user = $this->discord->users->create($response->user, true);
                    $this->discord->users->pushItem($user);
                }

                $users->pushItem($user);
            }

            return $users;
        });
    }

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

    /**
     * Returns the channel attribute.
     *
     * @return Channel|null The channel in which the scheduled event will be hosted, or null.
     */
    protected function getChannelAttribute(): ?Channel
    {
        if (! isset($this->attributes['channel_id'])) {
            return null;
        }

        if ($guild = $this->guild) {
            if ($channel = $guild->channels->get('id', $this->channel_id)) {
                return $channel;
            }
        }

        return $this->discord->getChannel($this->attributes['channel_id']);
    }

    /**
     * Returns the image attribute.
     *
     * @param string|null $format The image format.
     * @param int         $size   The size of the image.
     *
     * @return string|null The URL to the guild scheduled event cover image if exists.
     */
    public function getImageAttribute(?string $format = null, int $size = 1024): ?string
    {
        if (! isset($this->attributes['image'])) {
            return null;
        }

        static $allowed = ['png', 'jpg', 'webp'];

        if (! in_array(strtolower($format), $allowed)) {
            $format = 'png';
        }

        return "https://cdn.discordapp.com/guild-events/{$this->id}/{$this->attributes['image']}.{$format}?size={$size}";
    }

    /**
     * Returns the image hash.
     *
     * @return string|null The guild scheduled event cover image hash if exists.
     */
    protected function getImageHashAttribute(): ?string
    {
        return $this->attributes['image'] ?? null;
    }

    /**
     * Returns the Scheduled Start Time attribute.
     *
     * @return Carbon The time the scheduled event will start.
     *
     * @throws \Exception
     */
    protected function getScheduledStartTimeAttribute(): Carbon
    {
        return $this->attributeCarbonHelper('scheduled_start_time');
    }

    /**
     * Returns the Scheduled End Time attribute.
     *
     * @return Carbon|null The time the scheduled event will end, required if entity_type is EXTERNAL.
     *
     * @throws \Exception
     */
    protected function getScheduledEndTimeAttribute(): ?Carbon
    {
        return $this->attributeCarbonHelper('scheduled_end_time');
    }

    /**
     * Gets the entity metadata attribute.
     *
     * @return EntityMetadata|null The entity metadata for the guild scheduled event.
     */
    protected function getEntityMetadataAttribute(): ?EntityMetadata
    {
        return $this->attributePartHelper('entity_metadata', EntityMetadata::class);
    }

    /**
     * Gets the user that created the scheduled event.
     *
     * @return User|null The user that created the scheduled event.
     */
    protected function getCreatorAttribute(): ?User
    {
        if (isset($this->attributes['creator_id']) && $user = $this->discord->users->get('id', $this->attributes['creator_id'])) {
            return $user;
        }

        if (isset($this->attributes['creator'])) {
            if ($user = $this->discord->users->get('id', $this->attributes['creator']->id)) {
                return $user;
            }
        }

        return $this->attributePartHelper('creator', User::class);
    }

    /**
     * Gets the recurrence rule attribute.
     *
     * @return RecurrenceRule|null The recurrence rule for the guild scheduled event.
     */
    protected function getRecurrenceRuleAttribute(): ?RecurrenceRule
    {
        return $this->attributePartHelper('recurrence_rule', RecurrenceRule::class);
    }

    /**
     * Gets the guild scheduled event exceptions attribute.
     *
     * @return ExCollectionInterface The exceptions to the recurrence rule of the guild scheduled event.
     */
    protected function getGuildScheduledEventExceptionsAttribute(): ExCollectionInterface
    {
        return $this->attributeCollectionHelper('guild_scheduled_event_exceptions', ScheduledEventException::class, 'event_id');
    }

    /**
     * @inheritDoc
     *
     * @link https://discord.com/developers/docs/resources/guild-scheduled-event#create-guild-scheduled-event-json-params
     */
    public function getCreatableAttributes(): array
    {
        return [
            'channel_id' => $this->channel_id,
            'entity_metadata' => $this->entity_metadata,
            'name' => $this->name,
            'privacy_level' => $this->privacy_level,
            'scheduled_start_time' => $this->attributes['scheduled_start_time'],
            'scheduled_end_time' => $this->attributes['scheduled_end_time'],
            'description' => $this->description,
            'entity_type' => $this->entity_type,
            'image' => $this->image_hash,
        ];
    }

    /**
     * @inheritDoc
     *
     * @link https://discord.com/developers/docs/resources/guild-scheduled-event#modify-guild-scheduled-event-json-params
     */
    public function getUpdatableAttributes(): array
    {
        $attr = [
            'name' => $this->name,
            'privacy_level' => $this->privacy_level,
            'scheduled_start_time' => $this->attributes['scheduled_start_time'],
            'scheduled_end_time' => $this->attributes['scheduled_end_time'],
            'entity_type' => $this->entity_type,
            'status' => $this->status,
            'image' => $this->image_hash,
        ];

        if (array_key_exists('channel_id', $this->attributes)) {
            $attr['channel_id'] = $this->channel_id;
        }

        if (array_key_exists('entity_metadata', $this->attributes)) {
            $attr['entity_metadata'] = $this->entity_metadata;
        }

        if (array_key_exists('description', $this->attributes)) {
            $attr['description'] = $this->description;
        }

        return $attr;
    }

    /**
     * Gets the originating repository of the part.
     *
     * @since 10.42.0
     *
     * @throws \Exception If the part does not have an originating repository.
     *
     * @return ScheduledEventRepository|null The repository, or null if required part data is missing.
     */
    public function getRepository(): ScheduledEventRepository|null
    {
        if (! isset($this->attributes['guild_id'])) {
            return null;
        }

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

        return $guild->guild_scheduled_events;
    }

    /**
     * @inheritDoc
     */
    public function save(?string $reason = null): PromiseInterface
    {
        if (isset($this->attributes['guild_id'])) {
            /** @var Guild $guild */
            $guild = $this->guild ?? $this->factory->part(Guild::class, ['id' => $this->attributes['guild_id']], true);
            if ($botperms = $guild->getBotPermissions()) {
                if ($this->creator_id === $this->discord->id) {
                    if (! $botperms->create_events && ! $botperms->manage_events) {
                        return reject(new NoPermissionsException("You do not have permission to create or manage scheduled events in guild {$this->guild_id}."));
                    }
                } elseif ($this->created) {
                    if (! $botperms->create_events) {
                        return reject(new NoPermissionsException("You do not have permission to create scheduled events in the guild {$guild->id}."));
                    }
                } elseif (! $botperms->manage_events) {
                    return reject(new NoPermissionsException("You do not have permission to manage scheduled events in the guild {$guild->id}."));
                }
            }

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

        return parent::save();
    }

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