<?php
namespace filesystem\adapter;

use League\Flysystem\Config;
use League\Flysystem\FilesystemAdapter;
use League\Flysystem\FileAttributes;
use League\Flysystem\UnableToCopyFile;
use League\Flysystem\UnableToDeleteFile;
use League\Flysystem\UnableToMoveFile;
use League\Flysystem\UnableToReadFile;
use League\Flysystem\UnableToRetrieveMetadata;
use League\Flysystem\UnableToSetVisibility;
use League\Flysystem\UnableToWriteFile;
use Qiniu\Auth;
use Qiniu\Storage\UploadManager;
use Qiniu\Storage\BucketManager;

class QiniuAdapter implements FilesystemAdapter
{
    protected ?Auth $authMgr;
    protected ?UploadManager $uploadMgr;
    protected ?BucketManager $bucketMgr;

    public function __construct(protected string $access_key, protected string $secret_key, protected string $bucket, protected string $base_url, protected string $path)
    {
    }

    private function getAuthMgr(): Auth
    {
        return $this->authMgr ?? new Auth($this->access_key, $this->secret_key);
    }

    private function getUploadMgr(): UploadManager
    {
        return $this->uploadMgr ?? new UploadManager();
    }

    private function getBucketMgr(): BucketManager
    {
        return $this->bucketMgr ?? new BucketManager($this->authMgr);
    }

    private function getPathPrefix(): string
    {
        $path = ltrim($this->path, '\\/');
        if ($path !== '' && !str_ends_with($path, '/')) {
            $path = $path . '/';
        }

        return $path;
    }

    private function applyPathPrefix(string $path): string
    {
        $path = ltrim($path, '\\/');
        return $this->getPathPrefix() . $path;
    }

    private static function parseUrl($url): array
    {
        $result = [];

        // Build arrays of values we need to decode before parsing
        $entities = [
            '%21',
            '%2A',
            '%27',
            '%28',
            '%29',
            '%3B',
            '%3A',
            '%40',
            '%26',
            '%3D',
            '%24',
            '%2C',
            '%2F',
            '%3F',
            '%23',
            '%5B',
            '%5D',
            '%5C'
        ];
        $replacements = ['!', '*', "'", '(', ')', ';', ':', '@', '&', '=', '$', ',', '/', '?', '#', '[', ']', '/'];

        // Create encoded URL with special URL characters decoded so it can be parsed
        // All other characters will be encoded
        $encodedURL = str_replace($entities, $replacements, urlencode($url));

        // Parse the encoded URL
        $encodedParts = parse_url($encodedURL);

        // Now, decode each value of the resulting array
        if ($encodedParts) {
            foreach ($encodedParts as $key => $value) {
                $result[$key] = urldecode(str_replace($replacements, $entities, $value));
            }
        }

        return $result;
    }

    private function normalizeHost($domain): string
    {
        if (0 !== stripos($domain, 'https://') && 0 !== stripos($domain, 'http://')) {
            $domain = "http://{$domain}";
        }

        return rtrim($domain, '/') . '/';
    }

    private function getUrl(string $path): string
    {
        $segments = $this->parseUrl($path);
        $query = empty($segments['query']) ? '' : '?' . $segments['query'];

        return $this->normalizeHost($this->base_url) . ltrim(implode('/', array_map('rawurlencode', explode('/', $segments['path']))), '/') . $query;
    }

    private function privateDownloadUrl(string $path, int $expires = 3600): string
    {
        return $this->getAuthMgr()->privateDownloadUrl($this->getUrl($path), $expires);
    }

    private function getMetadata($path): FileAttributes|array
    {
        $result = $this->getBucketMgr()->stat($this->bucket, $path);
        $result[0]['key'] = $path;

        return $this->normalizeFileInfo($result[0]);
    }

    private function normalizeFileInfo(array $stats): FileAttributes
    {
        return new FileAttributes(
            $stats['key'],
            $stats['fsize'] ?? null,
            null,
            isset($stats['putTime']) ? floor($stats['putTime'] / 10000000) : null,
            $stats['mimeType'] ?? null
        );
    }

    public function fileExists(string $path): bool
    {
        [, $error] = $this->getBucketMgr()->stat($this->bucket, $this->applyPathPrefix($path));
        return is_null($error);
    }

    public function directoryExists(string $path): bool
    {
        return $this->fileExists($path);
    }

    public function write(string $path, string $contents, Config $config): void
    {
        $mime = $config->get('mime', 'application/octet-stream');

        /**
         * @var Error|null $error
         */
        [, $error] = $this->getUploadMgr()->put(
            $this->getAuthMgr()->uploadToken($this->bucket),
            $this->applyPathPrefix($path),
            $contents,
            null,
            $mime,
            $path
        );

        if ($error) {
            throw UnableToWriteFile::atLocation($path, $error->message());
        }
    }

    public function writeStream(string $path, $resource, Config $config): void
    {
        $data = '';

        while (!feof($resource)) {
            $data .= fread($resource, 1024);
        }

        $this->write($path, $data, $config);
    }

    public function read(string $path): string
    {
        try {
            $result = file_get_contents($this->privateDownloadUrl($path));
        } catch (\Exception $th) {
            throw UnableToReadFile::fromLocation($path);
        }

        if (false === $result) {
            throw UnableToReadFile::fromLocation($path);
        }

        return $result;
    }

    public function readStream(string $path)
    {
        if (ini_get('allow_url_fopen')) {
            if ($result = fopen($this->privateDownloadUrl($path), 'r')) {
                return $result;
            }
        }

        throw UnableToReadFile::fromLocation($path);
    }

    public function delete(string $path): void
    {
        [, $error] = $this->getBucketMgr()->delete($this->bucket, $this->applyPathPrefix($path));
        if (!is_null($error)) {
            throw UnableToDeleteFile::atLocation($path);
        }
    }

    public function deleteDirectory(string $path): void
    {
        $this->delete($path);
    }

    public function createDirectory(string $path, Config $config): void
    {
    }

    public function setVisibility(string $path, string $visibility): void
    {
        throw UnableToSetVisibility::atLocation($path);
    }

    public function visibility(string $path): FileAttributes
    {
        throw UnableToRetrieveMetadata::visibility($path);
    }

    public function mimeType(string $path): FileAttributes
    {
        $meta = $this->getMetadata($path);

        if ($meta->mimeType() === null) {
            throw UnableToRetrieveMetadata::mimeType($path);
        }

        return $meta;
    }

    public function lastModified(string $path): FileAttributes
    {
        $meta = $this->getMetadata($path);

        if ($meta->lastModified() === null) {
            throw UnableToRetrieveMetadata::lastModified($path);
        }
        return $meta;
    }

    public function fileSize(string $path): FileAttributes
    {
        $meta = $this->getMetadata($path);

        if ($meta->fileSize() === null) {
            throw UnableToRetrieveMetadata::fileSize($path);
        }
        return $meta;
    }

    public function listContents(string $path, bool $deep): iterable
    {
        $result = $this->getBucketMgr()->listFiles($this->bucket, $path);

        foreach ($result[0]['items'] ?? [] as $files) {
            yield $this->normalizeFileInfo($files);
        }
    }

    public function move(string $source, string $destination, Config $config): void
    {
        [, $error] = $this->getBucketMgr()->rename($this->bucket, $source, $destination);
        if (!is_null($error)) {
            throw UnableToMoveFile::fromLocationTo($source, $destination);
        }
    }

    public function copy(string $source, string $destination, Config $config): void
    {
        [, $error] = $this->getBucketMgr()->copy($this->bucket, $source, $this->bucket, $destination);
        if (!is_null($error)) {
            throw UnableToCopyFile::fromLocationTo($source, $destination);
        }
    }
}