<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\ThirdPartyBundle\Entity;

use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Civility;
use Chill\MainBundle\Entity\User;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ReadableCollection;
use Doctrine\ORM\Mapping as ORM;
use libphonenumber\PhoneNumber;
use Symfony\Component\Serializer\Annotation\Context;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * ThirdParty is a party recorded in the database.
 *
 * A party may be attached to multiple centers. Being attach to a center allow
 * all users with the right 'CHILL_3PARTY_3PARTY_SEE', 'CHILL_3PARTY_3 to see, select and edit parties for this
 * center.
 *
 * A ThirdParty may have:
 *
 * * 0, one or more categories;
 * * 0, one or more type
 * * 1 kind.
 *
 * ## Kind
 *
 * The kind indicate if a thirdparty is a:
 *
 * * company ("personne morale");
 * * a contact ("personne morale")
 * * a child inside a company ("contact" in French). Only companies may have childs
 *
 * **take care** the french translation may lead to misinterpretation, as the string "contact" is the translation of
 * kind "child".
 *
 * ## Categories and types
 *
 * ThirdParty may have zero, one or more categories and/or type.
 *
 * Categories are managed in the database. The Chill administrator may create or desactivate categories.
 *
 * Types are also a way to categorize the thirdparties, but this is done **into the code**, through
 *
 * @see{Chill\ThirdPartyBundle\ThirdPartyType\ThirdPartyTypeProviderInterface}. The type is stored into the
 * database by a Php array, mapped by a jsonb into the database. This has one advantage: it is easily searchable.
 *
 * As the list of type is hardcoded into database, it is more easily searchable. (for chill 2.0, the
 *
 * @see{Chill\ThirdPartyBundle\Form\Type\PickThirdPartyDynamicType} does not support it yet, but
 * the legacy @see{Chill\ThirdPartyBundle\Form\Type\PickThirdPartyType} does.
 *
 * The difference between categories and types is transparent for user: they choose the same fields into the UI, without
 * noticing a difference.
 *
 * ## Validation
 *
 * When a validation is inserted / updated, do not forget to update the related ThirdPartyEdit.vue component and the associated
 * list of possible violations.
 */
#[ORM\Entity]
#[ORM\Table(name: 'chill_3party.third_party')]
#[DiscriminatorMap(typeProperty: 'type', mapping: ['thirdparty' => ThirdParty::class])]
class ThirdParty implements TrackCreationInterface, TrackUpdateInterface, \Stringable
{
    final public const KIND_CHILD = 'child';

    final public const KIND_COMPANY = 'company';

    final public const KIND_CONTACT = 'contact';

    /**
     * [fr] Sigle.
     */
    #[Groups(['read', 'write', 'docgen:read', 'docgen:read:3party:parent'])]
    #[ORM\Column(name: 'acronym', type: \Doctrine\DBAL\Types\Types::STRING, length: 64, nullable: true)]
    private ?string $acronym = '';

    /**
     * Soft-delete flag.
     */
    #[ORM\Column(name: 'active', type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['defaut' => true])]
    private bool $active = true;

    #[Groups(['read', 'write', 'docgen:read', 'docgen:read:3party:parent'])]
    #[ORM\ManyToOne(targetEntity: Address::class, cascade: ['persist', 'remove'])]
    #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
    #[Context(normalizationContext: ['groups' => 'docgen:read'], groups: ['docgen:read:3party:parent'])]
    private ?Address $address = null;

    /**
     * Canonicalized form composed of name, company name and acronym.
     *
     * This field is read-only, and is generated on database side.
     */
    #[ORM\Column(name: 'canonicalized', type: \Doctrine\DBAL\Types\Types::TEXT, options: ['default' => ''])]
    private ?string $canonicalized = '';

    /**
     * @var Collection<int, ThirdPartyCategory>
     */
    #[Groups(['docgen:read', 'docgen:read:3party:parent'])]
    #[ORM\ManyToMany(targetEntity: ThirdPartyCategory::class)]
    #[ORM\JoinTable(name: 'chill_3party.thirdparty_category', joinColumns: [new ORM\JoinColumn(name: 'thirdparty_id', referencedColumnName: 'id')], inverseJoinColumns: [new ORM\JoinColumn(name: 'category_id', referencedColumnName: 'id')])]
    #[Context(normalizationContext: ['groups' => 'docgen:read'], groups: ['docgen:read:3party:parent'])]
    private Collection $categories;

    /**
     * @var Collection<int, Center>
     */
    #[ORM\ManyToMany(targetEntity: Center::class)]
    #[ORM\JoinTable(name: 'chill_3party.party_center')]
    private Collection $centers;

    /**
     * Contact Persons: One Institutional ThirdParty has Many Contact Persons.
     *
     * @var Collection<int, ThirdParty>
     */
    #[Assert\Valid(traverse: true)]
    #[ORM\OneToMany(mappedBy: 'parent', targetEntity: ThirdParty::class, cascade: ['persist'], orphanRemoval: true)]
    private Collection $children;

    #[Context(normalizationContext: ['groups' => 'docgen:read'], groups: ['docgen:read:3party:parent'])]
    #[Groups(['read', 'write', 'docgen:read', 'docgen:read:3party:parent'])]
    #[ORM\ManyToOne(targetEntity: Civility::class)] // ORM\JoinColumn(name="civility", referencedColumnName="id", nullable=true)
    private ?Civility $civility = null;

    #[Groups(['read', 'write'])]
    #[ORM\Column(name: 'comment', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: true)]
    private ?string $comment = null;

    #[Groups(['read', 'docgen:read', 'docgen:read:3party:parent'])]
    #[ORM\Column(name: 'contact_data_anonymous', type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => false])]
    private bool $contactDataAnonymous = false;

    #[ORM\Column(name: 'created_at', type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: false)]
    private \DateTimeImmutable $createdAt;

    #[ORM\ManyToOne(targetEntity: User::class)]
    #[ORM\JoinColumn(name: 'created_by', referencedColumnName: 'id')]
    private ?User $createdBy = null;

    #[Assert\Email]
    #[Groups(['read', 'write', 'docgen:read', 'docgen:read:3party:parent'])]
    #[ORM\Column(name: 'email', type: \Doctrine\DBAL\Types\Types::STRING, length: 255, nullable: true)]
    private ?string $email = null;

    #[Groups(['read', 'write', 'docgen:read', 'docgen:read:3party:parent'])]
    #[ORM\Column(name: 'firstname', type: \Doctrine\DBAL\Types\Types::TEXT, options: ['default' => ''])]
    private ?string $firstname = '';

    #[Groups(['read', 'docgen:read', 'docgen:read:3party:parent'])]
    #[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)]
    #[ORM\Id]
    #[ORM\GeneratedValue(strategy: 'AUTO')]
    private ?int $id = null;

    #[Groups(['write', 'docgen:read', 'docgen:read:3party:parent'])]
    #[ORM\Column(name: 'kind', type: \Doctrine\DBAL\Types\Types::STRING, length: 20, options: ['default' => ''])]
    private string $kind = '';

    #[Assert\Length(min: 2)]
    #[Assert\NotNull]
    #[Assert\NotBlank]
    #[Groups(['read', 'write', 'docgen:read', 'docgen:read:3party:parent'])]
    #[ORM\Column(name: 'name', type: \Doctrine\DBAL\Types\Types::STRING, length: 255)]
    private string $name = '';

    /**
     * [fr] Raison sociale.
     */
    #[Groups(['read', 'write', 'docgen:read', 'docgen:read:3party:parent'])]
    #[ORM\Column(name: 'name_company', type: \Doctrine\DBAL\Types\Types::STRING, length: 255, nullable: true)]
    private ?string $nameCompany = '';

    /**
     * Institutional ThirdParty: Many Contact Persons have One Institutional ThirdParty.
     */
    #[Groups(['read', 'write', 'docgen:read'])]
    #[ORM\ManyToOne(targetEntity: ThirdParty::class, inversedBy: 'children')]
    #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id')]
    #[Context(normalizationContext: ['groups' => 'docgen:read:3party:parent'], groups: ['docgen:read'])]
    private ?ThirdParty $parent = null;

    /**
     * [fr] Qualité.
     */
    #[Groups(['read', 'write', 'docgen:read', 'docgen:read:3party:parent'])]
    #[ORM\Column(name: 'profession', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
    #[Context(normalizationContext: ['groups' => 'docgen:read'], groups: ['docgen:read:3party:parent'])]
    private string $profession = '';

    #[Groups(['read', 'write', 'docgen:read', 'docgen:read:3party:parent'])]
    #[ORM\Column(name: 'telephone', type: 'phone_number', nullable: true)]
    #[\Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber]
    private ?PhoneNumber $telephone = null;

    #[Groups(['read', 'write', 'docgen:read', 'docgen:read:3party:parent'])]
    #[ORM\Column(name: 'telephone2', type: 'phone_number', nullable: true)]
    #[\Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber]
    private ?PhoneNumber $telephone2 = null;

    #[ORM\Column(name: 'types', type: \Doctrine\DBAL\Types\Types::JSON, nullable: true)]
    private ?array $thirdPartyTypes = [];

    #[ORM\Column(name: 'updated_at', type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true)]
    private ?\DateTimeImmutable $updatedAt = null;

    #[ORM\ManyToOne(targetEntity: User::class)]
    #[ORM\JoinColumn(name: 'updated_by', referencedColumnName: 'id')]
    private ?User $updatedBy = null;

    /**
     * ThirdParty constructor.
     */
    public function __construct()
    {
        $this->centers = new ArrayCollection();
        $this->categories = new ArrayCollection();
        $this->children = new ArrayCollection();
    }

    public function __toString(): string
    {
        return $this->getName();
    }

    /**
     * Add a child and set the child as active.
     *
     * Method used in conjonction with getActiveChildren in form.
     *
     * @internal use the method addChild
     *
     * @return $this
     */
    public function addActiveChild(ThirdParty $child): self
    {
        $child->setActive(true);

        return $this->addChild($child);
    }

    /**
     * @return $this
     */
    public function addCategory(ThirdPartyCategory $category): self
    {
        if (!$this->categories->contains($category)) {
            $this->categories[] = $category;
        }

        foreach ($this->children as $child) {
            $child->addCategory($category);
        }

        return $this;
    }

    public function addCenter(Center $center)
    {
        if (false === $this->centers->contains($center)) {
            $this->centers->add($center);
        }
    }

    /**
     * @return $this
     */
    public function addChild(ThirdParty $child): self
    {
        $this->children[] = $child;
        $child->setParent($this)->setKind(ThirdParty::KIND_CHILD);

        return $this;
    }

    public function addThirdPartyTypes(?string $type): self
    {
        if (null === $type) {
            return $this;
        }

        if (!\in_array($type, $this->thirdPartyTypes ?? [], true)) {
            $this->thirdPartyTypes[] = $type;
        }

        foreach ($this->children as $child) {
            $child->addThirdPartyTypes($type);
        }

        return $this;
    }

    public function addTypesAndCategories($typeAndCategory): self
    {
        if ($typeAndCategory instanceof ThirdPartyCategory) {
            $this->addCategory($typeAndCategory);

            return $this;
        }

        if (\is_string($typeAndCategory)) {
            $this->addThirdPartyTypes($typeAndCategory);

            return $this;
        }

        throw new \UnexpectedValueException(sprintf('typeAndCategory should be a string or a %s', ThirdPartyCategory::class));
    }

    public function getAcronym(): ?string
    {
        return $this->acronym;
    }

    public function getActive(): bool
    {
        return $this->active;
    }

    /**
     * Get the children where active = true.
     */
    public function getActiveChildren(): ReadableCollection
    {
        return $this->children->filter(static fn (ThirdParty $tp) => $tp->getActive());
    }

    public function getAddress(): ?Address
    {
        if ($this->isChild()) {
            return $this->getParent()->getAddress();
        }

        return $this->address;
    }

    public function getCategories(): Collection
    {
        return $this->categories;
    }

    public function getCenters(): Collection
    {
        return $this->centers;
    }

    public function getChildren(): Collection
    {
        return $this->children;
    }

    public function getCivility(): ?Civility
    {
        return $this->civility;
    }

    /**
     * Get comment.
     */
    public function getComment(): ?string
    {
        return $this->comment;
    }

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

    /**
     * Get email.
     */
    public function getEmail(): ?string
    {
        return $this->email;
    }

    public function getFirstname(): string
    {
        return $this->firstname;
    }

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

    public function getKind(): ?string
    {
        return $this->kind;
    }

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

    public function getNameCompany(): ?string
    {
        return $this->nameCompany;
    }

    public function getParent(): ?ThirdParty
    {
        return $this->parent;
    }

    public function getProfession(): string
    {
        return $this->profession;
    }

    /**
     * Get telephone.
     */
    public function getTelephone(): ?PhoneNumber
    {
        return $this->telephone;
    }

    public function getTelephone2(): ?PhoneNumber
    {
        return $this->telephone2;
    }

    /**
     * Get type.
     */
    public function getThirdPartyTypes(): ?array
    {
        return $this->thirdPartyTypes ?? [];
    }

    public function getTypesAndCategories(): array
    {
        return \array_merge(
            $this->getCategories()->toArray(),
            $this->getThirdPartyTypes() ?? []
        );
    }

    /**
     * @return \DateTime|null
     */
    public function getUpdatedAt(): ?\DateTimeImmutable
    {
        return $this->updatedAt;
    }

    public function getUpdatedBy(): ?User
    {
        return $this->updatedBy;
    }

    #[Groups(['read', 'docgen:read', 'docgen:read:3party:parent'])]
    public function isChild(): bool
    {
        return null !== $this->parent;
    }

    public function isContactDataAnonymous(): bool
    {
        return $this->contactDataAnonymous;
    }

    /**
     * Mechanism to differentiate children and parents.
     */
    public function isLeaf(): bool
    {
        return 0 !== $this->children->count();
    }

    public function isParent(): bool
    {
        return !$this->isChild();
    }

    /**
     * mark the child as unactive, but keep the child existing in the
     * database. To effectively remove the child, use removeChild instead.
     *
     * @return $this
     */
    public function removeActiveChild(ThirdParty $child): self
    {
        $child->setActive(false);

        return $this;
    }

    /**
     * @return $this
     */
    public function removeCategory(ThirdPartyCategory $category): self
    {
        $this->categories->removeElement($category);

        foreach ($this->children as $child) {
            $child->removeCategory($category);
        }

        return $this;
    }

    public function removeCenter(Center $center)
    {
        if ($this->centers->contains($center)) {
            $this->centers->removeElement($center);
        }
    }

    /**
     * Remove the child from the database.
     *
     * If you want to keep the child into the database
     * but desactivate it, use removeActiveChildren instead.
     *
     * @return $this
     */
    public function removeChild(ThirdParty $child): self
    {
        $this->children->removeElement($child);

        return $this;
    }

    public function removeThirdPartyTypes(?string $type): self
    {
        if (null === $type) {
            return $this;
        }

        if (\in_array($type, $this->thirdPartyTypes ?? [], true)) {
            $this->thirdPartyTypes = \array_filter($this->thirdPartyTypes, fn ($e) => !\in_array($e, $this->thirdPartyTypes, true));
        }

        foreach ($this->children as $child) {
            $child->removeThirdPartyTypes($type);
        }

        return $this;
    }

    public function removeTypesAndCategories($typeAndCategory): self
    {
        if ($typeAndCategory instanceof ThirdPartyCategory) {
            $this->removeCategory($typeAndCategory);

            return $this;
        }

        if (\is_string($typeAndCategory)) {
            $this->removeThirdPartyTypes($typeAndCategory);

            return $this;
        }

        throw new \UnexpectedValueException(sprintf('typeAndCategory should be a string or a %s', ThirdPartyCategory::class));
    }

    public function setAcronym(?string $acronym = null): ThirdParty
    {
        $this->acronym = (string) $acronym;

        return $this;
    }

    /**
     * @return $this
     */
    public function setActive(bool $active)
    {
        $this->active = $active;

        foreach ($this->children as $child) {
            $child->setActive($active);
        }

        return $this;
    }

    /**
     * @return $this
     */
    public function setAddress(?Address $address = null)
    {
        $this->address = $address;

        return $this;
    }

    public function setCenters(Collection $centers): self
    {
        $this->centers = $centers;

        return $this;
    }

    public function setCivility(?Civility $civility): ThirdParty
    {
        $this->civility = $civility;

        return $this;
    }

    /**
     * Set comment.
     *
     * @return ThirdParty
     */
    public function setComment(?string $comment = null)
    {
        $this->comment = $comment;

        return $this;
    }

    public function setContactDataAnonymous(bool $contactDataAnonymous): ThirdParty
    {
        $this->contactDataAnonymous = $contactDataAnonymous;

        return $this;
    }

    /**
     * @param \DateTimeImmutable $createdAt
     *
     * @return $this
     */
    public function setCreatedAt(\DateTimeInterface $createdAt): ThirdParty
    {
        $this->createdAt = $createdAt;

        return $this;
    }

    public function setCreatedBy(User $user): TrackCreationInterface
    {
        $this->createdBy = $user;

        return $this;
    }

    /**
     * Set email.
     *
     * @return ThirdParty
     */
    public function setEmail(?string $email = null)
    {
        $this->email = trim((string) $email);

        return $this;
    }

    public function setFirstname(?string $firstname): self
    {
        $this->firstname = trim((string) $firstname);

        return $this;
    }

    public function setKind(?string $kind): ThirdParty
    {
        $this->kind = (string) $kind;

        return $this;
    }

    public function setName(?string $name): self
    {
        $this->name = (string) $name;

        return $this;
    }

    public function setNameCompany(?string $nameCompany): ThirdParty
    {
        $this->nameCompany = (string) $nameCompany;

        return $this;
    }

    public function setParent(?ThirdParty $parent): ThirdParty
    {
        $this->parent = $parent;

        return $this;
    }

    public function setProfession(?string $profession): self
    {
        $this->profession = (string) $profession;

        return $this;
    }

    /**
     * Set telephone.
     */
    public function setTelephone(?PhoneNumber $telephone = null): self
    {
        $this->telephone = $telephone;

        return $this;
    }

    public function setTelephone2(?PhoneNumber $telephone2 = null): self
    {
        $this->telephone2 = $telephone2;

        return $this;
    }

    /**
     * Set type.
     *
     * @return ThirdParty
     */
    public function setThirdPartyTypes(?array $type = [])
    {
        // remove all keys from the input data
        $this->thirdPartyTypes = \array_values($type);

        foreach ($this->children as $child) {
            $child->setThirdPartyTypes($type);
        }

        return $this;
    }

    public function setTypesAndCategories(array $typesAndCategories): self
    {
        $types = \array_filter($typesAndCategories, static fn ($item) => !$item instanceof ThirdPartyCategory);
        $this->setThirdPartyTypes($types);

        // handle categories
        foreach ($typesAndCategories as $t) {
            $this->addTypesAndCategories($t);
        }

        $categories = \array_filter($typesAndCategories, static fn ($item) => $item instanceof ThirdPartyCategory);
        $categoriesHashes = \array_map(static fn (ThirdPartyCategory $c) => \spl_object_hash($c), $categories);

        foreach ($categories as $c) {
            $this->addCategory($c);
        }

        foreach ($this->getCategories() as $t) {
            if (!\in_array(\spl_object_hash($t), $categoriesHashes, true)) {
                $this->removeCategory($t);
            }
        }

        return $this;
    }

    /**
     * @param \DateTimeImmutable $updatedAt
     *
     * @return $this
     */
    public function setUpdatedAt(\DateTimeInterface $updatedAt): ThirdParty
    {
        $this->updatedAt = $updatedAt;

        return $this;
    }

    /**
     * @return $this
     */
    public function setUpdatedBy(User $updatedBy): ThirdParty
    {
        $this->updatedBy = $updatedBy;

        return $this;
    }
}
