<?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\AsyncUpload\Driver\LocalStorage;

use Base64Url\Base64Url;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
use Chill\DocStoreBundle\Service\Cryptography\KeyGenerator;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Path;

class StoredObjectManager implements StoredObjectManagerInterface
{
    private readonly string $baseDir;

    private readonly Filesystem $filesystem;

    public function __construct(
        ParameterBagInterface $parameterBag,
        private readonly KeyGenerator $keyGenerator,
    ) {
        $this->baseDir = $parameterBag->get('chill_doc_store')['local_storage']['storage_path'];
        $this->filesystem = new Filesystem();
    }

    public function getLastModified(StoredObject|StoredObjectVersion $document): \DateTimeInterface
    {
        $version = $document instanceof StoredObject ? $document->getCurrentVersion() : $document;

        if (null === $version) {
            throw StoredObjectManagerException::storedObjectDoesNotContainsVersion();
        }

        $path = $this->buildPath($version->getFilename());

        if (false === $ts = filemtime($path)) {
            throw StoredObjectManagerException::unableToReadDocumentOnDisk($path);
        }

        return \DateTimeImmutable::createFromFormat('U', (string) $ts);
    }

    public function getContentLength(StoredObject|StoredObjectVersion $document): int
    {
        return strlen($this->read($document));
    }

    public function exists(StoredObject|StoredObjectVersion $document): bool
    {
        $version = $document instanceof StoredObject ? $document->getCurrentVersion() : $document;

        if (null === $version) {
            return false;
        }

        return $this->existsContent($version->getFilename());
    }

    public function read(StoredObject|StoredObjectVersion $document): string
    {
        $version = $document instanceof StoredObject ? $document->getCurrentVersion() : $document;

        if (null === $version) {
            throw StoredObjectManagerException::storedObjectDoesNotContainsVersion();
        }

        $content = $this->readContent($version->getFilename());

        if (!$this->isVersionEncrypted($version)) {
            return $content;
        }

        $clearData = openssl_decrypt(
            $content,
            self::ALGORITHM,
            // TODO: Why using this library and not use base64_decode() ?
            Base64Url::decode($version->getKeyInfos()['k']),
            \OPENSSL_RAW_DATA,
            pack('C*', ...$version->getIv())
        );

        if (false === $clearData) {
            throw StoredObjectManagerException::unableToDecrypt(openssl_error_string());
        }

        return $clearData;
    }

    public function write(StoredObject $document, string $clearContent, ?string $contentType = null): StoredObjectVersion
    {
        $newIv = $document->isEncrypted() ? $document->getIv() : $this->keyGenerator->generateIv();
        $newKey = $document->isEncrypted() ? $document->getKeyInfos() : $this->keyGenerator->generateKey(self::ALGORITHM);
        $newType = $contentType ?? $document->getType();
        $version = $document->registerVersion(
            $newIv,
            $newKey,
            $newType
        );

        $encryptedContent = $this->isVersionEncrypted($version)
            ? openssl_encrypt(
                $clearContent,
                self::ALGORITHM,
                // TODO: Why using this library and not use base64_decode() ?
                Base64Url::decode($version->getKeyInfos()['k']),
                \OPENSSL_RAW_DATA,
                pack('C*', ...$version->getIv())
            )
            : $clearContent;

        if (false === $encryptedContent) {
            throw StoredObjectManagerException::unableToEncryptDocument((string) openssl_error_string());
        }

        $this->writeContent($version->getFilename(), $encryptedContent);

        return $version;
    }

    public function readContent(string $filename): string
    {
        $path = $this->buildPath($filename);

        if (!file_exists($path)) {
            throw StoredObjectManagerException::unableToFindDocumentOnDisk($path);
        }

        if (false === $content = file_get_contents($path)) {
            throw StoredObjectManagerException::unableToReadDocumentOnDisk($path);
        }

        return $content;
    }

    public function writeContent(string $filename, string $encryptedContent): void
    {
        $fullPath = $this->buildPath($filename);

        try {
            $this->filesystem->dumpFile($fullPath, $encryptedContent);
        } catch (IOExceptionInterface $exception) {
            throw StoredObjectManagerException::unableToStoreDocumentOnDisk($exception);
        }
    }

    public function existsContent(string $filename): bool
    {
        $path = $this->buildPath($filename);

        return $this->filesystem->exists($path);
    }

    private function buildPath(string $filename): string
    {
        $dirs = [$this->baseDir];

        for ($i = 0; $i < min(strlen($filename), 8); ++$i) {
            $dirs[] = $filename[$i];
        }

        $dirs[] = $filename;

        return Path::canonicalize(implode(DIRECTORY_SEPARATOR, $dirs));
    }

    public function delete(StoredObjectVersion $storedObjectVersion): void
    {
        if (!$this->exists($storedObjectVersion)) {
            return;
        }

        $path = $this->buildPath($storedObjectVersion->getFilename());

        $this->filesystem->remove($path);
        $this->removeDirectoriesRecursively(Path::getDirectory($path));
    }

    private function removeDirectoriesRecursively(string $path): void
    {
        if ($path === $this->baseDir) {
            return;
        }

        $files = scandir($path);

        // if it does contains only "." and "..", we can remove the directory
        if (2 === count($files) && in_array('.', $files, true) && in_array('..', $files, true)) {
            $this->filesystem->remove($path);
            $this->removeDirectoriesRecursively(Path::getDirectory($path));
        }
    }

    /**
     * @throws StoredObjectManagerException
     */
    public function etag(StoredObject|StoredObjectVersion $document): string
    {
        return md5($this->read($document));
    }

    public function clearCache(): void
    {
        // there is no cache: nothing to do here !
    }

    private function isVersionEncrypted(StoredObjectVersion $storedObjectVersion): bool
    {
        return $storedObjectVersion->isEncrypted();
    }
}
