<?php

/*
 * This file is part of the overtrue/flysystem-cos.
 * (c) overtrue <i@overtrue.me>
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace Overtrue\Flysystem\Cos;

use GuzzleHttp\Client as HttpClient;
use League\Flysystem\Adapter\AbstractAdapter;
use League\Flysystem\Adapter\CanOverwriteFiles;
use League\Flysystem\AdapterInterface;
use League\Flysystem\Config;
use Qcloud\Cos\Client;
use Qcloud\Cos\Exception\NoSuchKeyException;

/**
 * Class CosAdapter.
 */
class CosAdapter extends AbstractAdapter implements CanOverwriteFiles
{
    /**
     * @var Client
     */
    protected $client;

    /**
     * @var \GuzzleHttp\Client
     */
    protected $httpClient;

    /**
     * @var array
     */
    protected $config;

    /**
     * CosAdapter constructor.
     *
     * @param array $config
     */
    public function __construct(array $config = [])
    {
        $this->config = $config;

        if (!empty($this->config['cdn'])) {
            $this->setPathPrefix($this->config['cdn']);
        }
    }

    /**
     * @return string
     */
    public function getBucket()
    {
        return $this->config['bucket'];
    }

    /**
     * @return string
     */
    public function getAppId()
    {
        return $this->config['credentials']['appId'] ?? null;
    }

    /**
     * @return string
     */
    public function getRegion()
    {
        return $this->config['region'] ?? '';
    }

    /**
     * @param string $path
     *
     * @return string
     */
    public function getSourcePath($path)
    {
        return sprintf('%s.cos.%s.myqcloud.com/%s',
            $this->getBucket(), $this->getRegion(), $path
        );
    }

    /**
     * @param string $path
     *
     * @return string
     */
    public function getUrl($path)
    {
        if (!empty($this->config['cdn'])) {
            return $this->applyPathPrefix($path);
        }

        $options = [
            'Scheme' => $this->config['scheme'] ?? 'http',
        ];

        return $this->getClient()->getObjectUrl(
            $this->getBucket(), $path, null, $options
        );
    }

    /**
     * @param string     $path
     * @param string|int $expiration
     * @param array      $options
     *
     * @return string
     */
    public function getTemporaryUrl($path, $expiration, array $options = [])
    {
        $options = array_merge($options, ['Scheme' => $this->config['scheme'] ?? 'http']);

        $expiration = date('c', !\is_numeric($expiration) ? \strtotime($expiration) : \intval($expiration));

        $objectUrl = $this->getClient()->getObjectUrl(
            $this->getBucket(), $path, $expiration, $options
        );

        $url = parse_url($objectUrl);

        if ($this->config['cdn']) {
            return \sprintf('%s/%s?%s', \rtrim($this->config['cdn'], '/'), urldecode($url['path']), $url['query']);
        }

        return $objectUrl;
    }

    /**
     * @param string $path
     * @param string $contents
     * @param Config $config
     *
     * @return array|false
     */
    public function write($path, $contents, Config $config)
    {
        $options = $this->getUploadOptions($config);

        return $this->getClient()->upload($this->getBucket(), $path, $contents, $options);
    }

    /**
     * @param string   $path
     * @param resource $resource
     * @param Config   $config
     *
     * @return array|false
     */
    public function writeStream($path, $resource, Config $config)
    {
        $options = $this->getUploadOptions($config);

        return $this->getClient()->upload(
            $this->getBucket(),
            $path,
            stream_get_contents($resource, -1, 0),
            $options
        );
    }

    /**
     * @param string $path
     * @param string $contents
     * @param Config $config
     *
     * @return array|bool
     */
    public function update($path, $contents, Config $config)
    {
        return $this->write($path, $contents, $config);
    }

    /**
     * @param string   $path
     * @param resource $resource
     * @param Config   $config
     *
     * @return array|bool
     */
    public function updateStream($path, $resource, Config $config)
    {
        return $this->writeStream($path, $resource, $config);
    }

    /**
     * @param string $path
     * @param string $to
     *
     * @return bool
     */
    public function rename($path, $to)
    {
        $result = $this->copy($path, $to);

        $this->delete($path);

        return $result;
    }

    /**
     * @param string $path
     * @param string $to
     *
     * @return bool
     */
    public function copy($path, $to)
    {
        $source = [
            'Region' => $this->getRegion(),
            'Bucket' => $this->getBucket(),
            'Key' => $path,
        ];

        return (bool) $this->getClient()->copy($this->getBucket(), $to, $source);
    }

    /**
     * @param string $path
     *
     * @return bool
     */
    public function delete($path)
    {
        return (bool) $this->getClient()->deleteObject([
            'Bucket' => $this->getBucket(),
            'Key' => $path,
        ]);
    }

    /**
     * @param string $dirname
     *
     * @return bool
     */
    public function deleteDir($dirname)
    {
        $response = $this->listObjects($dirname);

        if (empty($response['Contents'])) {
            return true;
        }

        $keys = array_map(function ($item) {
            return ['Key' => $item['Key']];
        }, (array) $response['Contents']);

        return (bool) $this->getClient()->deleteObjects([
            'Bucket' => $this->getBucket(),
            'Objects' => $keys,
        ]);
    }

    /**
     * @param string $dirname
     * @param Config $config
     *
     * @return array|bool
     */
    public function createDir($dirname, Config $config)
    {
        return $this->getClient()->putObject([
            'Bucket' => $this->getBucket(),
            'Key' => $dirname.'/',
            'Body' => '',
        ]);
    }

    /**
     * @param string $path
     * @param string $visibility
     *
     * @return array|false
     */
    public function setVisibility($path, $visibility)
    {
        return (bool) $this->getClient()->PutObjectAcl([
            'Bucket' => $this->getBucket(),
            'Key' => $path,
            'ACL' => $this->normalizeVisibility($visibility),
        ]);
    }

    /**
     * @param string $path
     *
     * @return bool
     */
    public function has($path)
    {
        try {
            return (bool) $this->getMetadata($path);
        } catch (\Throwable $e) {
            return false;
        } catch (\Exception $e) {
            return false;
        }
    }

    /**
     * @param string $path
     *
     * @return array|bool
     */
    public function read($path)
    {
        try {
            if ($this->config['read_from_cdn']) {
                $response = $this->getHttpClient()
                    ->get($this->getTemporaryUrl($path, date('+5 min')))
                    ->getBody()
                    ->getContents();
            } else {
                $response = $this->getClient()->getObject([
                    'Bucket' => $this->getBucket(),
                    'Key' => $path,
                ])['Body'];
            }

            return ['contents' => (string) $response];
        } catch (\Throwable $e) {
            return null;
        } catch (\Exception $e) {
            return null;
        }
    }

    public function getClient()
    {
        return $this->client ?: $this->client = new Client($this->config);
    }

    /**
     * @param \Qcloud\Cos\Client $client
     *
     * @return $this
     */
    public function setClient(Client $client)
    {
        $this->client = $client;

        return $this;
    }

    /**
     * @return \GuzzleHttp\Client
     */
    public function getHttpClient()
    {
        return $this->httpClient ?: $this->httpClient = new HttpClient();
    }

    /**
     * @param \GuzzleHttp\Client $client
     *
     * @return $this
     */
    public function setHttpClient(HttpClient $client)
    {
        $this->httpClient = $client;

        return $this;
    }

    /**
     * @param string $path
     *
     * @return array|bool
     */
    public function readStream($path)
    {
        try {
            $temporaryUrl = $this->getTemporaryUrl($path, \strtotime('+5 min'));

            $stream = $this->getHttpClient()
                ->get($temporaryUrl, ['stream' => true])
                ->getBody()
                ->detach();

            return ['stream' => $stream];
        } catch (\Throwable $e) {
            return false;
        } catch (\Exception $e) {
            return false;
        }
    }

    /**
     * @param string $directory
     * @param bool   $recursive
     *
     * @return array|bool
     */
    public function listContents($directory = '', $recursive = false)
    {
        $list = [];

        $response = $this->listObjects($directory, $recursive);

        foreach ((array) $response->get('Contents') as $content) {
            $list[] = $this->normalizeFileInfo($content);
        }

        return $list;
    }

    /**
     * @param string $path
     *
     * @return array|bool
     */
    public function getMetadata($path)
    {
        return $this->getClient()->headObject([
            'Bucket' => $this->getBucket(),
            'Key' => $path,
        ]);
    }

    /**
     * @param string $path
     *
     * @return array|bool
     */
    public function getSize($path)
    {
        $meta = $this->getMetadata($path);

        return isset($meta['ContentLength']) ? ['size' => $meta['ContentLength']] : false;
    }

    /**
     * @param string $path
     *
     * @return array|bool
     */
    public function getMimetype($path)
    {
        $meta = $this->getMetadata($path);

        return isset($meta['ContentType']) ? ['mimetype' => $meta['ContentType']] : false;
    }

    /**
     * @param string $path
     *
     * @return array|bool
     */
    public function getTimestamp($path)
    {
        $meta = $this->getMetadata($path);

        return isset($meta['LastModified']) ? ['timestamp' => strtotime($meta['LastModified'])] : false;
    }

    /**
     * @param string $path
     *
     * @return array|bool
     */
    public function getVisibility($path)
    {
        $meta = $this->getClient()->getObjectAcl([
            'Bucket' => $this->getBucket(),
            'Key' => $path,
        ]);

        foreach ($meta->get('Grants') as $grant) {
            if ('READ' === $grant['Permission'] && false !== strpos($grant['Grantee']['URI'] ?? '', 'global/AllUsers')) {
                return ['visibility' => AdapterInterface::VISIBILITY_PUBLIC];
            }
        }

        return ['visibility' => AdapterInterface::VISIBILITY_PRIVATE];
    }

    /**
     * @param array $content
     *
     * @return array
     */
    protected function normalizeFileInfo(array $content)
    {
        $path = pathinfo($content['Key']);

        return [
            'type' => '/' === substr($content['Key'], -1) ? 'dir' : 'file',
            'path' => $content['Key'],
            'size' => \intval($content['Size']),
            'dirname' => \strval($path['dirname']),
            'basename' => \strval($path['basename']),
            'filename' => strval($path['filename']),
            'timestamp' => \strtotime($content['LastModified']),
            'extension' => $path['extension'] ?? '',
        ];
    }

    /**
     * @param string $directory
     * @param bool   $recursive
     *
     * @return mixed
     */
    protected function listObjects($directory = '', $recursive = false)
    {
        return $this->getClient()->listObjects([
            'Bucket' => $this->getBucket(),
            'Prefix' => ('' === (string) $directory) ? '' : ($directory.'/'),
            'Delimiter' => $recursive ? '' : '/',
        ]);
    }

    /**
     * @param Config $config
     *
     * @return array
     */
    protected function getUploadOptions(Config $config)
    {
        $options = [];

        if ($config->has('params')) {
            $options['params'] = $config['params'];
        }

        if ($config->has('visibility')) {
            $options['params']['ACL'] = $this->normalizeVisibility($config['visibility']);
        }

        return $options;
    }

    /**
     * @param $visibility
     *
     * @return string
     */
    protected function normalizeVisibility($visibility)
    {
        switch ($visibility) {
            case AdapterInterface::VISIBILITY_PUBLIC:
                $visibility = 'public-read';
                break;
        }

        return $visibility;
    }
}
