<?php

declare(strict_types=1);

namespace Workspace\Domain\Entities;

use Billing\Domain\Entities\SubscriptionEntity;
use Billing\Domain\ValueObjects\Count;
use DateTime;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Exception;
use Shared\Domain\ValueObjects\Id;
use Traversable;
use User\Domain\Entities\UserEntity;
use Workspace\Domain\Exceptions\InvitationNotFoundException;
use Workspace\Domain\Exceptions\MemberAlreadyJoinedException;
use Workspace\Domain\Exceptions\WorkspaceUserNotFoundException;
use Workspace\Domain\ValueObjects\Address;
use Workspace\Domain\ValueObjects\Email;
use Workspace\Domain\ValueObjects\Name;

#[ORM\Entity]
#[ORM\Table(name: 'workspace')]
#[ORM\HasLifecycleCallbacks]
class WorkspaceEntity
{
    /** A unique numeric identifier of the entity. */
    #[ORM\Embedded(class: Id::class, columnPrefix: false)]
    private Id $id;

    #[ORM\Embedded(class: Name::class, columnPrefix: false)]
    private Name $name;

    #[ORM\Embedded(class: Count::class, columnPrefix: 'credit_')]
    protected Count $creditCount;

    #[ORM\Column(type: Types::BOOLEAN, name: 'is_trialed', options: ['default' => false])]
    private bool $isTrialed = false;

    #[ORM\Column(type: Types::JSON, name: "address", nullable: true)]
    protected null|array|Address $address = null;

    /** Creation date and time of the entity */
    #[ORM\Column(type: 'datetime', name: 'created_at')]
    private DateTimeInterface $createdAt;

    /** The date and time when the entity was last modified. */
    #[ORM\Column(type: 'datetime', name: 'updated_at', nullable: true)]
    private ?DateTimeInterface $updatedAt = null;

    #[ORM\ManyToOne(targetEntity: UserEntity::class, inversedBy: 'ownedWorkspaces')]
    private UserEntity $owner;

    /** @var Collection<int,UserEntity> */
    #[ORM\ManyToMany(targetEntity: UserEntity::class, inversedBy: 'workspaces')]
    #[ORM\JoinTable(name: 'workspace_user')]
    #[ORM\JoinColumn(name: 'workspace_id', referencedColumnName: 'id')]
    #[ORM\InverseJoinColumn(name: 'user_id', referencedColumnName: 'id')]
    private Collection $users;

    /** @var Collection<int,WorkspaceInvitationEntity> */
    #[ORM\OneToMany(targetEntity: WorkspaceInvitationEntity::class, mappedBy: 'workspace', orphanRemoval: true, cascade: ['persist', 'remove'])]
    private Collection $invitations;

    /** @var Collection<int,SubscriptionEntity> */
    #[ORM\OneToMany(targetEntity: SubscriptionEntity::class, mappedBy: 'workspace', cascade: ['persist', 'remove'])]
    private Collection $subscriptions;

    #[ORM\OneToOne(targetEntity: SubscriptionEntity::class)]
    #[ORM\JoinColumn(onDelete: 'SET NULL')]
    private ?SubscriptionEntity $subscription = null;

    public function __construct(UserEntity $user, Name $name)
    {
        $this->id = new Id();
        $this->name = $name;
        $this->creditCount = new Count(0);
        $this->createdAt = new DateTime();
        $this->owner = $user;
        $this->users = new ArrayCollection();
        $this->invitations = new ArrayCollection();
        $this->subscriptions = new ArrayCollection();
    }

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

    public function getName(): Name
    {
        return $this->name;
    }

    public function setName(Name $name): self
    {
        $this->name = $name;
        return $this;
    }

    public function getCreditCount(): Count
    {
        return $this->creditCount;
    }

    public function getTotalCreditCount(): Count
    {
        $sub = $this->getSubscription();

        $totalCreditCount = $this->getCreditCount()->value;
        if (!is_null($totalCreditCount) && $sub) {
            $subCreditCount = $sub->getCredit()->value;

            $totalCreditCount = is_null($subCreditCount)
                ? null : (float) $totalCreditCount + (float) $subCreditCount;
        }

        return new Count(is_null($totalCreditCount) ? null : (float) $totalCreditCount);
    }

    public function addCredits(Count $count): self
    {
        if (is_null($count->value) || is_null($this->creditCount->value)) {
            $this->creditCount = new Count(); // Unlimited
            return $this;
        }

        $this->creditCount = new Count(
            (float) $this->creditCount->value + (float) $count->value
        );

        return $this;
    }

    public function deductCredit(Count $count): self
    {
        if ($this->subscription) {
            $count = $this->subscription->deductCredit($count);
        }

        if ((float) $count->value <= 0) {
            return $this;
        }

        if ($this->creditCount->value === null) {
            // Has unlimited add-on credits
            return $this;
        }

        if ((float) $this->creditCount->value > (float) $count->value) {
            $this->creditCount = new Count(
                (float) $this->creditCount->value - (float) $count->value
            );

            return $this;
        }

        $this->creditCount = new Count(0);
        return $this;
    }

    public function isEligibleForTrial(): bool
    {
        return !$this->isTrialed;
    }

    public function isEligibleForFreePlan(): bool
    {
        $owner = $this->owner;

        foreach ($owner->getOwnedWorkspaces() as $ws) {
            if (!$ws->getSubscription()) {
                continue;
            }

            if ($ws->getId()->equals($this->id)) {
                continue;
            }

            if ($ws->getSubscription()->getPlan()->getPrice()->value == 0) {
                return false;
            }
        }

        return true;
    }

    public function getAddress(): ?Address
    {
        if (is_array($this->address)) {
            $this->address = new Address($this->address);
        }

        return $this->address;
    }

    public function setAddress(Address $address): self
    {
        $this->address = $address;
        return $this;
    }

    public function getCreatedAt(): DateTimeInterface
    {
        return $this->createdAt;
    }

    public function getUpdatedAt(): ?DateTimeInterface
    {
        return $this->updatedAt;
    }

    public function getOwner(): UserEntity
    {
        return $this->owner;
    }

    public function setOwner(UserEntity|Id $owner): self
    {
        if ($owner instanceof Id) {
            $owner = $this->getUserById($owner);
        }

        if (
            $owner->getId()->getValue()->toString()
            === $this->owner->getId()->getValue()->toString()
        ) {
            return $this;
        }

        $this->addUser($this->owner);
        $this->removeUser($owner);
        $this->owner = $owner;

        return $this;
    }

    /**
     * @return Traversable<UserEntity>
     * @throws Exception
     */
    public function getUsers(): Traversable
    {
        return $this->users->getIterator();
    }

    public function addUser(UserEntity $user): self
    {
        $this->users->add($user);
        return $this;
    }

    /**
     * @throws WorkspaceUserNotFoundException
     * @throws Exception
     */
    public function removeUser(UserEntity|Id $user): self
    {
        if ($user instanceof Id) {
            $user = $this->getUserById($user);
        }

        $this->users->removeElement($user);
        $user->setCurrentWorkspace($user->getOwnedWorkspaces()[0]);
        return $this;
    }

    /**
     * @throws WorkspaceUserNotFoundException
     */
    private function getUserById(Id $id): UserEntity
    {
        /** @var UserEntity */
        foreach ($this->users as $user) {
            if ($user->getId()->getValue() == $id->getValue()) {
                return $user;
            }
        }

        throw new WorkspaceUserNotFoundException($id);
    }

    #[ORM\PreUpdate]
    public function preUpdate(): void
    {
        $this->updatedAt = new DateTime();
    }

    /**
     * @return Traversable<WorkspaceInvitationEntity>
     * @throws Exception
     */
    public function getInvitations(): Traversable
    {
        return $this->invitations->getIterator();
    }

    /**
     * @throws MemberAlreadyJoinedException
     */
    public function invite(Email $email): WorkspaceInvitationEntity
    {
        if ($this->owner->getEmail()->value == $email->value) {
            throw new MemberAlreadyJoinedException($email);
        }

        // Check if member is already in workspace
        foreach ($this->users as $user) {
            if ($user->getEmail()->value == $email->value) {
                throw new MemberAlreadyJoinedException($email);
            }
        }

        foreach ($this->invitations as $invitation) {
            if ($invitation->getEmail()->value == $email->value) {
                return $invitation;
            }
        }

        $invitation = new WorkspaceInvitationEntity($this, $email);
        $this->invitations->add($invitation);

        return $invitation;
    }

    /**
     * @throws InvitationNotFoundException
     */
    public function removeInvitation(WorkspaceInvitationEntity|Id $invitation): self
    {
        if ($invitation instanceof Id) {
            $invitation = $this->getInvitationById($invitation);
        }

        $this->invitations->removeElement($invitation);
        return $this;
    }

    /**
     * @throws InvitationNotFoundException
     */
    public function acceptInvitation(
        WorkspaceInvitationEntity|Id $invitation,
        UserEntity $user
    ): self {
        if ($invitation instanceof Id) {
            $invitation = $this->getInvitationById($invitation);
        }

        if ($invitation->getEmail()->value != $user->getEmail()->value) {
            throw new InvitationNotFoundException($invitation->getId());
        }

        $this->invitations->removeElement($invitation);
        $this->addUser($user);

        return $this;
    }

    /**
     * @throws InvitationNotFoundException
     */
    public function getInvitationById(Id $id): WorkspaceInvitationEntity
    {
        /** @var WorkspaceInvitationEntity */
        foreach ($this->invitations as $invitation) {
            if ($invitation->getId()->getValue() == $id->getValue()) {
                return $invitation;
            }
        }

        throw new InvitationNotFoundException($id);
    }

    public function addSubscription(SubscriptionEntity $sub): self
    {
        $this->subscriptions->add($sub);
        return $this;
    }

    public function subscribe(SubscriptionEntity $sub): self
    {
        $this->subscription = $sub;

        if ($sub->getTrialPeriodDays()->value > 0) {
            $this->isTrialed = true;
        }

        return $this;
    }

    public function removeSubscription(): self
    {
        $this->subscription = null;
        return $this;
    }

    public function getSubscription(): ?SubscriptionEntity
    {
        return $this->subscription;
    }
}
