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

use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Selectable;
use Doctrine\ORM\Mapping as ORM;
use Random\RandomException;

/**
 * Store each version of StoredObject's.
 *
 * A version should not be created manually: use the method @see{StoredObject::registerVersion} instead.
 *
 * Each filename must be unique within the same StoredObject. We add a condition on id to apply this condition only for
 * newly created versions when this new index is applied.
 */
#[ORM\Entity]
#[ORM\Table('chill_doc.stored_object_version')]
#[ORM\UniqueConstraint(name: 'chill_doc_stored_object_version_unique_by_object', columns: ['stored_object_id', 'version'])]
#[ORM\UniqueConstraint(name: 'chill_doc_stored_object_version_unique_by_filename', columns: ['filename'], options: ['where' => '(id > 0)'])]
class StoredObjectVersion implements TrackCreationInterface
{
    use TrackCreationTrait;

    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
    private ?int $id = null;

    /**
     * filename of the version in the stored object.
     */
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
    private string $filename = '';

    /**
     * @var Collection<int, StoredObjectPointInTime>&Selectable<int, StoredObjectPointInTime>
     */
    #[ORM\OneToMany(mappedBy: 'objectVersion', targetEntity: StoredObjectPointInTime::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
    private Collection&Selectable $pointInTimes;

    /**
     * Previous storedObjectVersion, from which the current stored object version is created.
     *
     * If null, the current stored object version is generated by other means.
     *
     * Those version may be associated with the same storedObject, or not. In this last case, that means that
     * the stored object's current version is created from another stored object version.
     */
    #[ORM\ManyToOne(targetEntity: StoredObjectVersion::class)]
    private ?StoredObjectVersion $createdFrom = null;

    /**
     * List of stored object versions created from the current version.
     *
     * @var Collection<int, StoredObjectVersion>
     */
    #[ORM\OneToMany(mappedBy: 'createdFrom', targetEntity: StoredObjectVersion::class)]
    private Collection $children;

    public function __construct(
        /**
         * The stored object associated with this version.
         */
        #[ORM\ManyToOne(targetEntity: StoredObject::class, inversedBy: 'versions')]
        #[ORM\JoinColumn(name: 'stored_object_id', nullable: false)]
        private StoredObject $storedObject,

        /**
         * The incremental version.
         */
        #[ORM\Column(name: 'version', type: \Doctrine\DBAL\Types\Types::INTEGER, options: ['default' => 0])]
        private int $version = 0,

        /**
         * vector for encryption.
         *
         * @var int[]
         */
        #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, name: 'iv')]
        private array $iv = [],

        /**
         * Key infos for document encryption.
         *
         * @var array
         */
        #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, name: 'key')]
        private array $keyInfos = [],

        /**
         * type of the document.
         */
        #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, name: 'type', options: ['default' => ''])]
        private string $type = '',
        ?string $filename = null,
    ) {
        $this->filename = $filename ?? self::generateFilename($this);
        $this->pointInTimes = new ArrayCollection();
        $this->children = new ArrayCollection();
    }

    public static function generateFilename(StoredObjectVersion $storedObjectVersion): string
    {
        try {
            $suffix = base_convert(bin2hex(random_bytes(8)), 16, 36);
        } catch (RandomException) {
            $suffix = uniqid(more_entropy: true);
        }

        return $storedObjectVersion->getStoredObject()->getPrefix().'/'.$suffix;
    }

    public function getFilename(): string
    {
        return $this->filename;
    }

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

    public function getIv(): array
    {
        return $this->iv;
    }

    public function getKeyInfos(): array
    {
        return $this->keyInfos;
    }

    public function getStoredObject(): StoredObject
    {
        return $this->storedObject;
    }

    public function getType(): string
    {
        return $this->type;
    }

    public function getVersion(): int
    {
        return $this->version;
    }

    /**
     * @return Collection<int, StoredObjectPointInTime>&Selectable<int, StoredObjectPointInTime>
     */
    public function getPointInTimes(): Selectable&Collection
    {
        return $this->pointInTimes;
    }

    public function hasPointInTimes(): bool
    {
        return $this->pointInTimes->count() > 0;
    }

    /**
     * @internal use @see{StoredObjectPointInTime} constructor instead
     */
    public function addPointInTime(StoredObjectPointInTime $storedObjectPointInTime): self
    {
        if (!$this->pointInTimes->contains($storedObjectPointInTime)) {
            $this->pointInTimes->add($storedObjectPointInTime);
        }

        return $this;
    }

    public function removePointInTime(StoredObjectPointInTime $storedObjectPointInTime): self
    {
        if ($this->pointInTimes->contains($storedObjectPointInTime)) {
            $this->pointInTimes->removeElement($storedObjectPointInTime);
        }

        return $this;
    }

    public function getCreatedFrom(): ?StoredObjectVersion
    {
        return $this->createdFrom;
    }

    public function setCreatedFrom(?StoredObjectVersion $createdFrom): StoredObjectVersion
    {
        if (null === $createdFrom && null !== $this->createdFrom) {
            $this->createdFrom->removeChild($this);
        }

        $createdFrom?->addChild($this);

        $this->createdFrom = $createdFrom;

        return $this;
    }

    public function addChild(StoredObjectVersion $child): self
    {
        if (!$this->children->contains($child)) {
            $this->children->add($child);
        }

        return $this;
    }

    public function removeChild(StoredObjectVersion $child): self
    {
        $result = $this->children->removeElement($child);

        if (false === $result) {
            throw new \UnexpectedValueException('the child is not associated with the current stored object version');
        }

        return $this;
    }

    public function isEncrypted(): bool
    {
        return ([] !== $this->getKeyInfos()) && ([] !== $this->getIv());
    }
}
