<?php

namespace Velis\Filesystem\Adapter;

use Aws\S3\Exception\S3Exception;
use Aws\S3\S3Client;
use finfo;
use Gaufrette\Exception\FileNotFound;
use Gaufrette\FilesystemInterface as GaufretteFilesystemInterface;
use Psr\SimpleCache\InvalidArgumentException;
use Velis\Cache\CacheInterface;
use Velis\Exception;
use Velis\Filesystem\Av\Contract\AvClientInterface;
use Velis\Filesystem\Av\Exception\OneOrMoreFilesInfectedException;
use WideImage\WideImage;

/**
 * @author Jan Małysiak <jan.malysiak@velis.pl>
 */
class AwsS3 extends GaufretteFilesystemWrapper
{
    private const CACHE_PREFIX = 'file.';

    /**
     * @var S3Client
     */
    private $client;

    /**
     * @var string
     */
    private string $bucketName;

    private CacheInterface $cache;

    private int $ttl;

    /**
     * @param GaufretteFilesystemInterface $filesystem
     * @param S3Client $client
     * @param string $bucketName
     * @param CacheInterface $cache
     * @param int $ttl
     */
    public function __construct(
        GaufretteFilesystemInterface $filesystem,
        S3Client $client,
        string $bucketName,
        CacheInterface $cache,
        int $ttl,
        private readonly ?AvClientInterface $avClient = null
    ) {
        parent::__construct($filesystem);

        $this->client = $client;
        $this->bucketName = $bucketName;
        $this->cache = $cache;
        $this->ttl = $ttl;
    }

    /**
     * {@inheritDoc}
     * @throws InvalidArgumentException
     */
    public function has($key)
    {
        $cacheKey = $this->getCacheKey($key);
        if ($this->cache->has($cacheKey)) {
            return true;
        }

        try {
            $fileExists = parent::has($key);
        } catch (S3Exception $exception) {
            $fileExists = parent::has($key);
        }

        if ($fileExists) {
            $this->cache->set($cacheKey, 1, $this->ttl);
        }

        return $fileExists;
    }

    /**
     * @param string $key
     * @return string
     */
    private function getCacheKey(string $key): string
    {
        return self::CACHE_PREFIX . md5($key);
    }

    /**
     * {@inheritDoc}
     * @throws InvalidArgumentException
     */
    public function rename($sourceKey, $targetKey)
    {
        $result = parent::rename($sourceKey, $targetKey);

        $this->cache->delete($this->getCacheKey($sourceKey));
        $this->cache->set($this->getCacheKey($targetKey), 1, $this->ttl);

        return $result;
    }

    /**
     * {@inheritDoc}
     * @throws InvalidArgumentException
     */
    public function write($key, $content, $overwrite = true)
    {
        $file = $this->get($key, true);

        $fileInfo = new finfo(FILEINFO_MIME_TYPE);
        $mimeType = $fileInfo->buffer($content);

        $result = $file->setContent($content, [
            'ContentType' => $mimeType,
        ]);

        if (false !== $result) {
            $cacheKey = $this->getCacheKey($key);
            $this->cache->set($cacheKey, 1, $this->ttl);
        }

        return $result;
    }

    /**
     * {@inheritDoc}
     * @throws InvalidArgumentException
     */
    protected function get($key, $create = false)
    {
        $cacheKey = $this->getCacheKey($key);

        try {
            $result = parent::get($key, $create);
            $this->cache->set($cacheKey, 1, $this->ttl);
        } catch (FileNotFound $e) {
            $this->cache->delete($cacheKey);
            throw $e;
        }

        return $result;
    }

    /**
     * {@inheritDoc}
     * @throws InvalidArgumentException
     */
    public function read($key)
    {
        $cacheKey = $this->getCacheKey($key);

        try {
            $content = parent::read($key);
            $this->cache->set($cacheKey, 1, $this->ttl);
        } catch (FileNotFound $e) {
            $this->cache->delete($cacheKey);
            throw $e;
        }

        return $content;
    }

    /**
     * {@inheritDoc}
     * @throws InvalidArgumentException
     */
    public function delete($key)
    {
        $result = parent::delete($key);
        $cacheKey = $this->getCacheKey($key);
        $this->cache->delete($cacheKey);

        return $result;
    }

    /**
     * {@inheritDoc}
     * @throws InvalidArgumentException
     * @throws Exception
     */
    public function getImageSize($key)
    {
        $imageStr = $this->read($key);
        if (!$imageStr) {
            Exception::raise(sprintf('File %s does not exist.', $key));
        }

        $image = WideImage::loadFromString($imageStr);

        $width = $image->getWidth();
        $height = $image->getHeight();

        return [
            $width,
            $height,
        ];
    }

    /**
     * {@inheritDoc}
     */
    public function upload($key, $tmpFileName)
    {
        $avResult = $this->avClient?->fileScanInStream($tmpFileName);

        if ($avResult?->isInfected()) {
            unlink($tmpFileName);
            throw new OneOrMoreFilesInfectedException();
        }

        $file = $this->get($key, true);

        $fileInfo = new finfo();
        $mimeType = $fileInfo->file($tmpFileName, FILEINFO_MIME_TYPE);

        $file->setContent(file_get_contents($tmpFileName), [
            'ContentType' => $mimeType,
        ]);

        unlink($tmpFileName);

        return true;
    }

    /**
     * {@inheritDoc}
     *
     * This function does nothing as Gaufrette does not support deleting directories at this moment:
     * https://github.com/KnpLabs/Gaufrette/issues/182
     */
    protected function rmdir($key)
    {
        return true;
    }

    /**
     * {@inheritDoc}
     * @throws InvalidArgumentException
     */
    public function symlink($sourceKey, $targetKey)
    {
        return $this->copy($sourceKey, $targetKey);
    }

    /**
     * {@inheritDoc}
     * @throws InvalidArgumentException
     */
    public function copy(string $sourceKey, string $targetKey): bool
    {
        $this->client->copy($this->bucketName, $sourceKey, $this->bucketName, $targetKey);
        $cacheKey = $this->getCacheKey($targetKey);
        $this->cache->set($cacheKey, 1, $this->ttl);

        return true;
    }
}
