<?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\MainBundle\Entity;

use Chill\MainBundle\Doctrine\Model\Point;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Context\ExecutionContextInterface;

/**
 * Address.
 */
#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
#[ORM\Table(name: 'chill_main_address')]
class Address implements TrackCreationInterface, TrackUpdateInterface
{
    use TrackCreationTrait;
    use TrackUpdateTrait;

    /**
     * When an Address does match with the AddressReference.
     */
    final public const ADDR_REFERENCE_STATUS_MATCH = 'match';

    /**
     * When an Address does not match with the AddressReference, and
     * is pending for a review.
     */
    final public const ADDR_REFERENCE_STATUS_TO_REVIEW = 'to_review';

    /**
     * When an Address does not match with the AddressReference, but
     * is reviewed.
     */
    final public const ADDR_REFERENCE_STATUS_REVIEWED = 'reviewed';

    #[Groups(['write'])]
    #[ORM\ManyToOne(targetEntity: AddressReference::class)]
    private ?AddressReference $addressReference = null;

    #[Groups(['write'])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
    private string $buildingName = '';

    #[Groups(['write'])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => false])]
    private bool $confidential = false;

    #[Groups(['write'])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
    private string $corridor = '';

    /**
     * used for the CEDEX information.
     */
    #[Groups(['write'])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
    private string $distribution = '';

    #[Groups(['write'])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
    private string $extra = '';

    #[Groups(['write'])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
    private string $flat = '';

    #[Groups(['write'])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
    private string $floor = '';

    /**
     * List of geographical units and addresses.
     *
     * This list is computed by a materialized view. It won't be populated until a refresh is done
     * on the materialized view.
     *
     * @var Collection<int, GeographicalUnit>
     *
     * @readonly
     */
    #[ORM\ManyToMany(targetEntity: GeographicalUnit::class)]
    #[ORM\JoinTable(name: 'view_chill_main_address_geographical_unit', joinColumns: [new ORM\JoinColumn(name: 'address_id')], inverseJoinColumns: [new ORM\JoinColumn(name: 'geographical_unit_id')])]
    private Collection $geographicalUnits;

    /**
     * @readonly
     */
    #[Groups(['write'])]
    #[ORM\Id]
    #[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)]
    #[ORM\GeneratedValue(strategy: 'AUTO')]
    private ?int $id = null;

    /**
     * True if the address is a "no address", aka homeless person, ...
     */
    #[Groups(['write'])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => false])]
    private bool $isNoAddress = false;

    /**
     * A ThirdParty reference for person's addresses that are linked to a third party.
     */
    #[Groups(['write'])]
    #[ORM\ManyToOne(targetEntity: ThirdParty::class)]
    #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
    private ?ThirdParty $linkedToThirdParty = null;

    /**
     * A geospatial field storing the coordinates of the Address.
     */
    #[Groups(['write'])]
    #[ORM\Column(type: 'point', nullable: true)]
    private ?Point $point = null;

    #[Groups(['write'])]
    #[ORM\ManyToOne(targetEntity: PostalCode::class)]
    #[ORM\JoinColumn(nullable: false)]
    private ?PostalCode $postcode = null;

    /**
     * @var self::ADDR_REFERENCE_STATUS_*
     */
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => self::ADDR_REFERENCE_STATUS_MATCH])]
    private string $refStatus = self::ADDR_REFERENCE_STATUS_MATCH;

    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: false, options: ['default' => 'CURRENT_TIMESTAMP'])]
    private \DateTimeImmutable $refStatusLastUpdate;

    #[Groups(['write'])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
    private string $steps = '';

    #[Groups(['write'])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
    private string $street = '';

    #[Groups(['write'])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
    private string $streetNumber = '';

    /**
     * Indicates when the address starts validation. Used to build an history
     * of address. By default, the current date.
     */
    #[Groups(['write'])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATE_MUTABLE)]
    private \DateTime $validFrom;

    /**
     * Indicates when the address ends. Used to build an history
     * of address.
     */
    #[Groups(['write'])]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATE_MUTABLE, nullable: true)]
    private ?\DateTime $validTo = null;

    public function __construct()
    {
        $this->validFrom = new \DateTime();
        $this->refStatusLastUpdate = new \DateTimeImmutable('now');
        $this->geographicalUnits = new ArrayCollection();
    }

    public static function createFromAddress(Address $original): Address
    {
        return (new Address())
            ->setAddressReference($original->getAddressReference())
            ->setBuildingName($original->getBuildingName())
            ->setConfidential($original->getConfidential())
            ->setCorridor($original->getCorridor())
            ->setDistribution($original->getDistribution())
            ->setExtra($original->getExtra())
            ->setFlat($original->getFlat())
            ->setFloor($original->getFloor())
            ->setIsNoAddress($original->getIsNoAddress())
            ->setLinkedToThirdParty($original->getLinkedToThirdParty())
            ->setPoint($original->getPoint())
            ->setPostcode($original->getPostcode())
            ->setSteps($original->getSteps())
            ->setStreet($original->getStreet())
            ->setStreetNumber($original->getStreetNumber())
            ->setValidFrom($original->getValidFrom())
            ->setValidTo($original->getValidTo());
    }

    public static function createFromAddressReference(AddressReference $original): Address
    {
        return (new Address())
            ->syncWithReference($original);
    }

    public function syncWithReference(AddressReference $addressReference): Address
    {
        $this
            ->setPoint($addressReference->getPoint())
            ->setPostcode($addressReference->getPostcode())
            ->setStreet($addressReference->getStreet())
            ->setStreetNumber($addressReference->getStreetNumber())
            ->setRefStatus(self::ADDR_REFERENCE_STATUS_MATCH)
            ->setAddressReference($addressReference);

        return $this;
    }

    public function getAddressReference(): ?AddressReference
    {
        return $this->addressReference;
    }

    public function getBuildingName(): string
    {
        return $this->buildingName;
    }

    public function getConfidential(): bool
    {
        return $this->confidential;
    }

    public function getCorridor(): string
    {
        return $this->corridor;
    }

    public function getDistribution(): string
    {
        return $this->distribution;
    }

    public function getExtra(): string
    {
        return $this->extra;
    }

    public function getFlat(): string
    {
        return $this->flat;
    }

    public function getFloor(): string
    {
        return $this->floor;
    }

    /**
     * @return Collection<int, GeographicalUnit>|GeographicalUnit[]
     */
    public function getGeographicalUnits(): Collection
    {
        return $this->geographicalUnits;
    }

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

    /**
     * Get IsNoAddress.
     *
     * Indicate true if the address is a fake address (homeless, ...)
     */
    public function getIsNoAddress(): bool
    {
        return $this->isNoAddress;
    }

    public function getLinkedToThirdParty()
    {
        return $this->linkedToThirdParty;
    }

    public function getPoint(): ?Point
    {
        return $this->point;
    }

    /**
     * Get postcode.
     */
    public function getPostcode(): ?PostalCode
    {
        return $this->postcode;
    }

    public function getRefStatus(): string
    {
        return $this->refStatus;
    }

    public function getRefStatusLastUpdate(): \DateTimeImmutable
    {
        return $this->refStatusLastUpdate;
    }

    public function getSteps(): string
    {
        return $this->steps;
    }

    public function getStreet(): string
    {
        return $this->street;
    }

    /**
     * Get streetAddress1 (legacy function).
     *
     * @return string
     *
     * @deprecated
     */
    public function getStreetAddress1()
    {
        return $this->street;
    }

    /**
     * Get streetAddress2 (legacy function).
     *
     * @return string
     *
     * @deprecated
     */
    public function getStreetAddress2()
    {
        return $this->streetNumber;
    }

    public function getStreetNumber(): string
    {
        return $this->streetNumber;
    }

    public function getValidFrom(): \DateTime
    {
        return $this->validFrom;
    }

    public function getValidTo(): ?\DateTimeInterface
    {
        return $this->validTo;
    }

    public function hasAddressReference(): bool
    {
        return null !== $this->getAddressReference();
    }

    public function isNoAddress(): bool
    {
        return $this->getIsNoAddress();
    }

    public function setAddressReference(?AddressReference $addressReference = null): Address
    {
        $this->addressReference = $addressReference;

        return $this;
    }

    public function setBuildingName(?string $buildingName): self
    {
        $this->buildingName = (string) $buildingName;

        return $this;
    }

    public function setConfidential(bool $confidential): self
    {
        $this->confidential = $confidential;

        return $this;
    }

    public function setCorridor(?string $corridor): self
    {
        $this->corridor = (string) $corridor;

        return $this;
    }

    public function setDistribution(?string $distribution): self
    {
        $this->distribution = (string) $distribution;

        return $this;
    }

    public function setExtra(?string $extra): self
    {
        $this->extra = (string) $extra;

        return $this;
    }

    public function setFlat(?string $flat): self
    {
        $this->flat = (string) $flat;

        return $this;
    }

    public function setFloor(?string $floor): self
    {
        $this->floor = (string) $floor;

        return $this;
    }

    /**
     * Set IsNoAddress.
     *
     * Indicate true if the address is a fake address (homeless, ...)
     *
     * @return $this
     */
    public function setIsNoAddress(bool $isNoAddress): self
    {
        $this->isNoAddress = $isNoAddress;

        return $this;
    }

    public function setLinkedToThirdParty(?ThirdParty $linkedToThirdParty): self
    {
        $this->linkedToThirdParty = $linkedToThirdParty;

        return $this;
    }

    public function setPoint(?Point $point): self
    {
        $this->point = $point;

        return $this;
    }

    /**
     * Set postcode.
     *
     * @return Address
     */
    public function setPostcode(?PostalCode $postcode = null)
    {
        $this->postcode = $postcode;

        return $this;
    }

    /**
     * Update the ref status.
     *
     * <<<<<<< HEAD
     *
     * @param Address::ADDR_REFERENCE_STATUS_* $refStatus
     * @param bool|null                        $updateLastUpdate Also update the "refStatusLastUpdate"
     *                                                           =======
     *                                                           The refstatuslast update is also updated
     *                                                           >>>>>>> 31152616d (Feature: Provide api endpoint for reviewing addresses)
     */
    public function setRefStatus(string $refStatus, ?bool $updateLastUpdate = true): self
    {
        $this->refStatus = $refStatus;

        if ($updateLastUpdate) {
            $this->setRefStatusLastUpdate(new \DateTimeImmutable('now'));
        }

        return $this;
    }

    public function setRefStatusLastUpdate(\DateTimeImmutable $refStatusLastUpdate): self
    {
        $this->refStatusLastUpdate = $refStatusLastUpdate;

        return $this;
    }

    public function setSteps(?string $steps): self
    {
        $this->steps = (string) $steps;

        return $this;
    }

    public function setStreet(?string $street): self
    {
        $this->street = (string) $street;

        return $this;
    }

    /**
     * Set streetAddress1 (legacy function).
     *
     * @deprecated
     */
    public function setStreetAddress1(?string $streetAddress1): self
    {
        $this->street = (string) $streetAddress1;

        return $this;
    }

    /**
     * Set streetAddress2 (legacy function).
     *
     * @deprecated
     */
    public function setStreetAddress2(?string $streetAddress2): self
    {
        $this->streetNumber = (string) $streetAddress2;

        return $this;
    }

    public function setStreetNumber(?string $streetNumber): self
    {
        $this->streetNumber = (string) $streetNumber;

        return $this;
    }

    /**
     * @return Address
     */
    public function setValidFrom(\DateTime $validFrom)
    {
        $this->validFrom = $validFrom;

        return $this;
    }

    public function setValidTo(?\DateTimeInterface $validTo = null): self
    {
        $this->validTo = $validTo;

        return $this;
    }

    /**
     * Validate the address.
     *
     * Check that:
     *
     * * if the address is not home address:
     *     * the postal code is present
     *     * the valid from is not null
     *     * the address street 1 is greater than 2
     *
     * @param array $payload
     */
    public function validate(ExecutionContextInterface $context, $payload)
    {
        if (!$this->getValidFrom() instanceof \DateTime) {
            $context
                ->buildViolation('address.date-should-be-set')
                ->atPath('validFrom')
                ->addViolation();
        }

        if ($this->isNoAddress()) {
            return;
        }

        if ('' === $this->getStreet()) {
            $context
                ->buildViolation('address.street1-should-be-set')
                ->atPath('streetAddress1')
                ->addViolation();
        }

        if (!$this->getPostcode() instanceof PostalCode) {
            $context
                ->buildViolation('address.postcode-should-be-set')
                ->atPath('postCode')
                ->addViolation();
        }
    }
}
