<?php

declare(strict_types=1);

namespace App\Module\ProjectManagement\Domain\Entity;

use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
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 App\Module\ProjectManagement\Infrastructure\ApiPlatform\State\TaskCalendarProcessor;
use App\Module\ProjectManagement\Infrastructure\ApiPlatform\State\TaskNumberProcessor;
use App\Module\ProjectManagement\Infrastructure\Doctrine\DoctrineTaskRepository;
use App\Shared\Domain\Contract\BlamableInterface;
use App\Shared\Domain\Contract\TaskInterface;
use App\Shared\Domain\Contract\TimestampableInterface;
use App\Shared\Domain\Contract\UserInterface;
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;

#[ApiResource(
    operations: [
        new GetCollection(paginationEnabled: false, security: "is_granted('project-management.tasks.view')"),
        new Get(security: "is_granted('project-management.tasks.view')"),
        new Post(security: "is_granted('project-management.tasks.manage')", processor: TaskNumberProcessor::class),
        new Patch(security: "is_granted('project-management.tasks.manage')", processor: TaskCalendarProcessor::class),
        new Delete(security: "is_granted('project-management.tasks.manage')", processor: TaskCalendarProcessor::class),
    ],
    normalizationContext: ['groups' => ['task:read']],
    denormalizationContext: ['groups' => ['task:write']],
    order: ['id' => 'DESC'],
)]
#[ApiFilter(SearchFilter::class, properties: ['project' => 'exact', 'group' => 'exact', 'assignee' => 'exact', 'collaborators' => 'exact', 'priority' => 'exact', 'effort' => 'exact', 'tags' => 'exact', 'status' => 'exact'])]
#[ApiFilter(DateFilter::class, properties: ['scheduledStart', 'scheduledEnd', 'deadline'])]
#[ApiFilter(BooleanFilter::class, properties: ['archived', 'syncToCalendar'])]
#[ApiFilter(OrderFilter::class, properties: ['scheduledStart', 'deadline'])]
#[ORM\Entity(repositoryClass: DoctrineTaskRepository::class)]
#[ORM\Table(name: 'task')]
#[ORM\UniqueConstraint(name: 'uniq_task_project_number', columns: ['project_id', 'number'])]
class Task implements TaskInterface, TimestampableInterface, BlamableInterface
{
    use TimestampableBlamableTrait;

    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    #[Groups(['task:read'])]
    private ?int $id = null;

    #[ORM\Column(type: 'integer')]
    #[Groups(['task:read'])]
    private ?int $number = null;

    #[ORM\Column(length: 255)]
    #[Groups(['task:read', 'task:write'])]
    private ?string $title = null;

    #[ORM\Column(type: 'text', nullable: true)]
    #[Groups(['task:read', 'task:write'])]
    private ?string $description = null;

    #[ORM\ManyToOne(targetEntity: TaskStatus::class)]
    #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
    #[Groups(['task:read', 'task:write'])]
    private ?TaskStatus $status = null;

    #[ORM\ManyToOne(targetEntity: TaskEffort::class)]
    #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
    #[Groups(['task:read', 'task:write'])]
    private ?TaskEffort $effort = null;

    #[ORM\ManyToOne(targetEntity: TaskPriority::class)]
    #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
    #[Groups(['task:read', 'task:write'])]
    private ?TaskPriority $priority = null;

    #[ORM\ManyToOne(targetEntity: UserInterface::class)]
    #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
    #[Groups(['task:read', 'task:write'])]
    private ?UserInterface $assignee = null;

    /** @var Collection<int, UserInterface> */
    #[ORM\ManyToMany(targetEntity: UserInterface::class)]
    #[ORM\JoinTable(
        name: 'task_collaborator',
        joinColumns: [new ORM\JoinColumn(name: 'task_id', referencedColumnName: 'id', onDelete: 'CASCADE')],
        inverseJoinColumns: [new ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', onDelete: 'CASCADE')],
    )]
    #[Groups(['task:read', 'task:write'])]
    private Collection $collaborators;

    #[ORM\ManyToOne(targetEntity: TaskGroup::class)]
    #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
    #[Groups(['task:read', 'task:write'])]
    private ?TaskGroup $group = null;

    #[ORM\ManyToOne(targetEntity: Project::class, inversedBy: 'tasks')]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    #[Groups(['task:read', 'task:write'])]
    #[Assert\NotNull]
    private ?Project $project = null;

    /** @var Collection<int, TaskTag> */
    #[ORM\ManyToMany(targetEntity: TaskTag::class)]
    #[ORM\JoinTable(
        name: 'task_task_type',
        joinColumns: [new ORM\JoinColumn(name: 'task_id', referencedColumnName: 'id')],
        inverseJoinColumns: [new ORM\JoinColumn(name: 'task_type_id', referencedColumnName: 'id')],
    )]
    #[Groups(['task:read', 'task:write'])]
    private Collection $tags;

    #[ORM\Column(type: 'boolean')]
    #[Groups(['task:read', 'task:write'])]
    private bool $archived = false;

    /** @var Collection<int, TaskDocument> */
    #[ORM\OneToMany(targetEntity: TaskDocument::class, mappedBy: 'task', cascade: ['remove'])]
    #[Groups(['task:read'])]
    private Collection $documents;

    #[ORM\Column(type: 'datetime_immutable', nullable: true)]
    #[Groups(['task:read', 'task:write'])]
    private ?DateTimeImmutable $scheduledStart = null;

    #[ORM\Column(type: 'datetime_immutable', nullable: true)]
    #[Groups(['task:read', 'task:write'])]
    private ?DateTimeImmutable $scheduledEnd = null;

    #[ORM\Column(type: 'datetime_immutable', nullable: true)]
    #[Groups(['task:read', 'task:write'])]
    private ?DateTimeImmutable $deadline = null;

    #[ORM\Column(type: 'boolean', options: ['default' => false])]
    #[Groups(['task:read', 'task:write'])]
    private bool $syncToCalendar = false;

    #[ORM\Column(length: 255, nullable: true)]
    private ?string $calendarEventUid = null;

    #[ORM\Column(length: 255, nullable: true)]
    private ?string $calendarTodoUid = null;

    #[ORM\Column(type: 'text', nullable: true)]
    #[Groups(['task:read'])]
    private ?string $calendarSyncError = null;

    #[ORM\ManyToOne(targetEntity: TaskRecurrence::class, inversedBy: 'tasks')]
    #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
    #[Groups(['task:read', 'task:write'])]
    private ?TaskRecurrence $recurrence = null;

    public function __construct()
    {
        $this->tags          = new ArrayCollection();
        $this->documents     = new ArrayCollection();
        $this->collaborators = new ArrayCollection();
    }

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

    public function getNumber(): ?int
    {
        return $this->number;
    }

    public function setNumber(int $number): static
    {
        $this->number = $number;

        return $this;
    }

    public function getTitle(): ?string
    {
        return $this->title;
    }

    public function setTitle(string $title): static
    {
        $this->title = $title;

        return $this;
    }

    public function getDescription(): ?string
    {
        return $this->description;
    }

    public function setDescription(?string $description): static
    {
        $this->description = $description;

        return $this;
    }

    public function getStatus(): ?TaskStatus
    {
        return $this->status;
    }

    public function setStatus(?TaskStatus $status): static
    {
        $this->status = $status;

        return $this;
    }

    public function getEffort(): ?TaskEffort
    {
        return $this->effort;
    }

    public function setEffort(?TaskEffort $effort): static
    {
        $this->effort = $effort;

        return $this;
    }

    public function getPriority(): ?TaskPriority
    {
        return $this->priority;
    }

    public function setPriority(?TaskPriority $priority): static
    {
        $this->priority = $priority;

        return $this;
    }

    public function getAssignee(): ?UserInterface
    {
        return $this->assignee;
    }

    public function setAssignee(?UserInterface $assignee): static
    {
        $this->assignee = $assignee;

        return $this;
    }

    /** @return Collection<int, UserInterface> */
    public function getCollaborators(): Collection
    {
        return $this->collaborators;
    }

    public function addCollaborator(UserInterface $user): static
    {
        if (!$this->collaborators->contains($user)) {
            $this->collaborators->add($user);
        }

        return $this;
    }

    public function removeCollaborator(UserInterface $user): static
    {
        $this->collaborators->removeElement($user);

        return $this;
    }

    public function getGroup(): ?TaskGroup
    {
        return $this->group;
    }

    public function setGroup(?TaskGroup $group): static
    {
        $this->group = $group;

        return $this;
    }

    public function getProject(): ?Project
    {
        return $this->project;
    }

    public function setProject(?Project $project): static
    {
        $this->project = $project;

        return $this;
    }

    /** @return Collection<int, TaskTag> */
    public function getTags(): Collection
    {
        return $this->tags;
    }

    public function addTag(TaskTag $tag): static
    {
        if (!$this->tags->contains($tag)) {
            $this->tags->add($tag);
        }

        return $this;
    }

    public function removeTag(TaskTag $tag): static
    {
        $this->tags->removeElement($tag);

        return $this;
    }

    public function isArchived(): bool
    {
        return $this->archived;
    }

    public function setArchived(bool $archived): static
    {
        $this->archived = $archived;

        return $this;
    }

    /** @return Collection<int, TaskDocument> */
    public function getDocuments(): Collection
    {
        return $this->documents;
    }

    public function getScheduledStart(): ?DateTimeImmutable
    {
        return $this->scheduledStart;
    }

    public function setScheduledStart(?DateTimeImmutable $scheduledStart): static
    {
        $this->scheduledStart = $scheduledStart;

        return $this;
    }

    public function getScheduledEnd(): ?DateTimeImmutable
    {
        return $this->scheduledEnd;
    }

    public function setScheduledEnd(?DateTimeImmutable $scheduledEnd): static
    {
        $this->scheduledEnd = $scheduledEnd;

        return $this;
    }

    public function getDeadline(): ?DateTimeImmutable
    {
        return $this->deadline;
    }

    public function setDeadline(?DateTimeImmutable $deadline): static
    {
        $this->deadline = $deadline;

        return $this;
    }

    public function isSyncToCalendar(): bool
    {
        return $this->syncToCalendar;
    }

    public function setSyncToCalendar(bool $syncToCalendar): static
    {
        $this->syncToCalendar = $syncToCalendar;

        return $this;
    }

    public function getCalendarEventUid(): ?string
    {
        return $this->calendarEventUid;
    }

    public function setCalendarEventUid(?string $calendarEventUid): static
    {
        $this->calendarEventUid = $calendarEventUid;

        return $this;
    }

    public function getCalendarTodoUid(): ?string
    {
        return $this->calendarTodoUid;
    }

    public function setCalendarTodoUid(?string $calendarTodoUid): static
    {
        $this->calendarTodoUid = $calendarTodoUid;

        return $this;
    }

    public function getCalendarSyncError(): ?string
    {
        return $this->calendarSyncError;
    }

    public function setCalendarSyncError(?string $calendarSyncError): static
    {
        $this->calendarSyncError = $calendarSyncError;

        return $this;
    }

    public function getRecurrence(): ?TaskRecurrence
    {
        return $this->recurrence;
    }

    public function setRecurrence(?TaskRecurrence $recurrence): static
    {
        $this->recurrence = $recurrence;

        return $this;
    }

    #[Assert\Callback]
    public function validateScheduledDates(ExecutionContextInterface $context): void
    {
        if ((null === $this->scheduledStart) !== (null === $this->scheduledEnd)) {
            $context->buildViolation('scheduledStart and scheduledEnd must both be set or both be null.')
                ->atPath('scheduledEnd')
                ->addViolation()
            ;
        }
        if (null !== $this->scheduledStart && null !== $this->scheduledEnd
            && $this->scheduledEnd <= $this->scheduledStart) {
            $context->buildViolation('scheduledEnd must be after scheduledStart.')
                ->atPath('scheduledEnd')
                ->addViolation()
            ;
        }
    }

    #[Assert\Callback]
    public function validateCollaborators(ExecutionContextInterface $context): void
    {
        if (null !== $this->assignee && $this->collaborators->contains($this->assignee)) {
            $context->buildViolation('The assignee cannot also be a collaborator.')
                ->atPath('collaborators')
                ->addViolation()
            ;
        }
    }

    #[Assert\Callback]
    public function validateStatusBelongsToProjectWorkflow(ExecutionContextInterface $context): void
    {
        if (null === $this->status || null === $this->project) {
            return;
        }

        $projectWorkflow = $this->project->getWorkflow();
        $statusWorkflow  = $this->status->getWorkflow();

        if (null === $projectWorkflow || null === $statusWorkflow) {
            return;
        }

        if ($projectWorkflow->getId() !== $statusWorkflow->getId()) {
            $context->buildViolation('Status does not belong to this project\'s workflow.')
                ->atPath('status')
                ->addViolation()
            ;
        }
    }
}
