<?php

namespace Mautic\WebhookBundle\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\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
use Mautic\CategoryBundle\Entity\Category;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\CoreBundle\Entity\FormEntity;
use Mautic\CoreBundle\Entity\SkipModifiedInterface;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Mapping\ClassMetadata;

#[ApiResource(
    shortName: 'Webhooks',
    operations: [
        new GetCollection(uriTemplate: '/webhooks', security: "is_granted('webhook:webhooks:viewown')"),
        new Post(uriTemplate: '/webhooks', security: "is_granted('webhook:webhooks:create')"),
        new Get(uriTemplate: '/webhooks/{id}', security: "is_granted('webhook:webhooks:viewown')"),
        new Put(uriTemplate: '/webhooks/{id}', security: "is_granted('webhook:webhooks:editown')"),
        new Patch(uriTemplate: '/webhooks/{id}', security: "is_granted('webhook:webhooks:editother')"),
        new Delete(uriTemplate: '/webhooks/{id}', security: "is_granted('webhook:webhooks:deleteown')"),
    ],
    normalizationContext: [
        'groups'                  => ['webhook:read'],
        'swagger_definition_name' => 'Read',
        'api_included'            => ['category'],
    ],
    denormalizationContext: [
        'groups'                  => ['webhook:write'],
        'swagger_definition_name' => 'Write',
    ]
)]
class Webhook extends FormEntity implements SkipModifiedInterface
{
    public const LOGS_DISPLAY_LIMIT = 100;

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

    /**
     * @var ?string
     */
    #[Groups(['webhook:read', 'webhook:write'])]
    private $name;

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

    /**
     * @var ?string
     */
    #[Groups(['webhook:read', 'webhook:write'])]
    private $webhookUrl;

    /**
     * @var ?string
     */
    #[Groups(['webhook:read', 'webhook:write'])]
    private $secret;

    /**
     * @var Category|null
     **/
    #[Groups(['webhook:read', 'webhook:write'])]
    private $category;

    /**
     * @var Collection<int, Event>
     */
    #[Groups(['webhook:read', 'webhook:write'])]
    private $events;

    /**
     * @var ArrayCollection<int, Log>
     */
    private $logs;

    /**
     * @var array
     */
    private $removedEvents = [];

    /**
     * @var mixed[]
     */
    #[Groups(['webhook:read', 'webhook:write'])]
    private $payload;

    /**
     * Holds a simplified array of events, just an array of event types.
     * It's used for API serializaiton.
     *
     * @var array
     */
    #[Groups(['webhook:read', 'webhook:write'])]
    private $triggers = [];

    /**
     * ASC or DESC order for fetching order of the events when queue mode is on.
     * Null means use the global default.
     *
     * @var string|null
     */
    #[Groups(['webhook:read', 'webhook:write'])]
    private $eventsOrderbyDir;

    private ?\DateTimeImmutable $markedUnhealthyAt      = null;
    private ?\DateTimeImmutable $unHealthySince         = null;
    private ?\DateTimeImmutable $lastNotificationSentAt = null;

    public function __construct()
    {
        $this->events = new ArrayCollection();
        $this->logs   = new ArrayCollection();
    }

    public static function loadMetadata(ORM\ClassMetadata $metadata): void
    {
        $builder = new ClassMetadataBuilder($metadata);
        $builder->setTable('webhooks')
            ->setCustomRepositoryClass(WebhookRepository::class);

        $builder->addIdColumns();

        $builder->addCategory();

        $builder->createOneToMany('events', 'Event')
            ->orphanRemoval()
            ->setIndexBy('eventType')
            ->mappedBy('webhook')
            ->cascadePersist()
            ->cascadeMerge()
            ->cascadeDetach()
            ->build();

        $builder->createOneToMany('logs', 'Log')->setOrderBy(['dateAdded' => Order::Descending->value])
            ->fetchExtraLazy()
            ->mappedBy('webhook')
            ->cascadePersist()
            ->cascadeMerge()
            ->cascadeDetach()
            ->build();

        $builder->addNamedField('webhookUrl', Types::TEXT, 'webhook_url');
        $builder->addField('secret', Types::STRING);
        $builder->addNullableField('eventsOrderbyDir', Types::STRING, 'events_orderby_dir');
        $builder->addNullableField('markedUnhealthyAt', Types::DATETIME_IMMUTABLE, 'marked_unhealthy_at');
        $builder->addNullableField('unHealthySince', Types::DATETIME_IMMUTABLE, 'unhealthy_since');
        $builder->addNullableField('lastNotificationSentAt', Types::DATETIME_IMMUTABLE, 'last_notification_sent_at');
    }

    /**
     * Prepares the metadata for API usage.
     */
    public static function loadApiMetadata(ApiMetadataDriver $metadata): void
    {
        $metadata->setGroupPrefix('hook')
            ->addListProperties(
                [
                    'id',
                    'name',
                    'description',
                    'webhookUrl',
                    'secret',
                    'eventsOrderbyDir',
                    'category',
                    'triggers',
                ]
            )
            ->build();
    }

    public static function loadValidatorMetadata(ClassMetadata $metadata): void
    {
        $metadata->addPropertyConstraint(
            'name',
            new NotBlank(
                [
                    'message' => 'mautic.core.name.required',
                ]
            )
        );

        $metadata->addPropertyConstraint(
            'webhookUrl',
            new Assert\Url(
                [
                    'message' => 'mautic.core.valid_url_required',
                ]
            )
        );

        $metadata->addPropertyConstraint(
            'webhookUrl',
            new NotBlank(
                [
                    'message' => 'mautic.core.valid_url_required',
                ]
            )
        );

        $metadata->addPropertyConstraint(
            'eventsOrderbyDir',
            new Assert\Choice(
                [
                    null,
                    Order::Ascending->value,
                    Order::Descending->value,
                ]
            )
        );
    }

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

    /**
     * @param string $name
     *
     * @return Webhook
     */
    public function setName($name)
    {
        $this->isChanged('name', $name);
        $this->name = $name;

        return $this;
    }

    /**
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @param string $description
     *
     * @return Webhook
     */
    public function setDescription($description)
    {
        $this->isChanged('description', $description);
        $this->description = $description;

        return $this;
    }

    /**
     * @return string
     */
    public function getDescription()
    {
        return $this->description;
    }

    /**
     * @param string $webhookUrl
     *
     * @return Webhook
     */
    public function setWebhookUrl($webhookUrl)
    {
        $this->isChanged('webhookUrl', $webhookUrl);
        $this->webhookUrl = $webhookUrl;

        return $this;
    }

    /**
     * @return string
     */
    public function getWebhookUrl()
    {
        return $this->webhookUrl;
    }

    /**
     * @param ?string $secret
     *
     * @return Webhook
     */
    public function setSecret($secret)
    {
        $this->isChanged('secret', $secret);
        $this->secret = $secret;

        return $this;
    }

    /**
     * @return ?string
     */
    public function getSecret()
    {
        return $this->secret;
    }

    /**
     * @return Webhook
     */
    public function setCategory(?Category $category = null)
    {
        $this->isChanged('category', $category);
        $this->category = $category;

        return $this;
    }

    /**
     * @return Category
     */
    public function getCategory()
    {
        return $this->category;
    }

    /**
     * @return Collection<int, Event>
     */
    public function getEvents()
    {
        return $this->events;
    }

    /**
     * @param Collection<int, Event> $events
     *
     * @return $this
     */
    public function setEvents($events)
    {
        $this->isChanged('events', $events);

        $this->events = $events;

        foreach ($events as $event) {
            $event->setWebhook($this);
        }

        return $this;
    }

    /**
     * This builds a simple array with subscribed events.
     */
    public function buildTriggers(): void
    {
        foreach ($this->events as $event) {
            $this->triggers[] = $event->getEventType();
        }
    }

    /**
     * Takes the array of triggers and builds events from them if they don't exist already.
     */
    public function setTriggers(array $triggers): void
    {
        foreach ($triggers as $key) {
            $this->addTrigger($key);
        }
    }

    /**
     * Takes a trigger (event type) and builds the Event object form it if it doesn't exist already.
     *
     * @param string $key
     */
    public function addTrigger($key): bool
    {
        if ($this->eventExists($key)) {
            return false;
        }

        $event = new Event();
        $event->setEventType($key);
        $event->setWebhook($this);
        $this->addEvent($event);

        return true;
    }

    /**
     * Check if an event exists comared to its type.
     *
     * @param string $key
     */
    public function eventExists($key): bool
    {
        foreach ($this->events as $event) {
            if ($event->getEventType() === $key) {
                return true;
            }
        }

        return false;
    }

    /**
     * @return $this
     */
    public function addEvent(Event $event)
    {
        $this->isChanged('events', $event);

        $this->events[] = $event;

        return $this;
    }

    /**
     * @return $this
     */
    public function removeEvent(Event $event)
    {
        $this->isChanged('events', $event);
        $this->removedEvents[] = $event;
        $this->events->removeElement($event);

        return $this;
    }

    /**
     * @param string $eventsOrderbyDir
     */
    public function setEventsOrderbyDir($eventsOrderbyDir)
    {
        $this->isChanged('eventsOrderbyDir', $eventsOrderbyDir);
        $this->eventsOrderbyDir = $eventsOrderbyDir;

        return $this;
    }

    /**
     * @return string
     */
    public function getEventsOrderbyDir()
    {
        return $this->eventsOrderbyDir;
    }

    /**
     * Get log entities.
     *
     * @return ArrayCollection<int,Log>
     */
    public function getLogs()
    {
        return $this->logs;
    }

    /**
     * @return Collection<int,Log>
     */
    public function getLimitedLogs(): Collection
    {
        $criteria = Criteria::create()
            ->setMaxResults(self::LOGS_DISPLAY_LIMIT);

        return $this->logs->matching($criteria);
    }

    /**
     * @param ArrayCollection<int,Log> $logs
     *
     * @return $this
     */
    public function addLogs($logs)
    {
        $this->logs = $logs;

        /** @var Log $log */
        foreach ($logs as $log) {
            $log->setWebhook($this);
        }

        return $this;
    }

    /**
     * @return $this
     */
    public function addLog(Log $log)
    {
        $this->logs[] = $log;

        return $this;
    }

    /**
     * @return $this
     */
    public function removeLog(Log $log)
    {
        $this->logs->removeElement($log);

        return $this;
    }

    /**
     * @return array
     */
    public function getPayload()
    {
        return $this->payload;
    }

    /**
     * @return Webhook
     */
    public function setPayload($payload)
    {
        $this->payload = $payload;

        return $this;
    }

    public function wasModifiedRecently(): bool
    {
        $dateModified = $this->getDateModified();

        if (null === $dateModified) {
            return false;
        }

        $aWhileBack = (new \DateTime())->modify('-2 days');

        if ($dateModified < $aWhileBack) {
            return false;
        }

        return true;
    }

    /**
     * @param string $prop
     */
    protected function isChanged($prop, $val)
    {
        $getter  = 'get'.ucfirst($prop);
        $current = $this->$getter();
        if ('category' == $prop) {
            $currentId = ($current) ? $current->getId() : '';
            $newId     = ($val) ? $val->getId() : null;
            if ($currentId != $newId) {
                $this->changes[$prop] = [$currentId, $newId];
            }
        } elseif ('events' == $prop) {
            $this->changes[$prop] = [];
        } elseif ($current != $val) {
            $this->changes[$prop] = [$current, $val];
        } else {
            parent::isChanged($prop, $val);
        }
    }

    public function getMarkedUnhealthyAt(): ?\DateTimeImmutable
    {
        return $this->markedUnhealthyAt;
    }

    public function setMarkedUnhealthyAt(?\DateTimeImmutable $markedUnhealthyAt): Webhook
    {
        $this->isChanged('markedUnhealthyAt', $markedUnhealthyAt);
        $this->markedUnhealthyAt = $markedUnhealthyAt;

        return $this;
    }

    public function getUnHealthySince(): ?\DateTimeImmutable
    {
        return $this->unHealthySince;
    }

    public function setUnHealthySince(?\DateTimeImmutable $unHealthySince): self
    {
        $this->unHealthySince = $unHealthySince;

        return $this;
    }

    public function getLastNotificationSentAt(): ?\DateTimeImmutable
    {
        return $this->lastNotificationSentAt;
    }

    public function setLastNotificationSentAt(?\DateTimeImmutable $lastNotificationSentAt): self
    {
        $this->lastNotificationSentAt = $lastNotificationSentAt;

        return $this;
    }

    /**
     * Do not update modified_by and date_modified fields if only DNC or manipulator was changed.
     * Avoid unnecessary update queries.
     */
    public function shouldSkipSettingModifiedProperties(): bool
    {
        $changes = $this->changes;

        unset($changes['markedUnhealthyAt']);
        unset($changes['unHealthySince']);
        unset($changes['lastNotificationSentAt']);

        return 0 === count($changes);
    }
}
