<?php

/**
 *
 * This file is part of phpFastCache.
 *
 * @license MIT License (MIT)
 *
 * For full copyright and license information, please see the docs/CREDITS.txt file.
 *
 * @author Khoa Bui (khoaofgod)  <khoaofgod@gmail.com> https://www.phpfastcache.com
 * @author Georges.L (Geolim4)  <contact@geolim4.com>
 *
 */
declare(strict_types=1);

namespace Phpfastcache\Drivers\Cassandra;

use Cassandra;
use Cassandra\Exception;
use Cassandra\Exception\InvalidArgumentException;
use Cassandra\Session as CassandraSession;
use DateTime;
use Phpfastcache\Cluster\AggregatablePoolInterface;
use Phpfastcache\Core\Pool\{DriverBaseTrait, ExtendedCacheItemPoolInterface};
use Phpfastcache\Entities\DriverStatistic;
use Phpfastcache\Exceptions\{PhpfastcacheInvalidArgumentException, PhpfastcacheLogicException};
use Psr\Cache\CacheItemInterface;


/**
 * Class Driver
 * @package phpFastCache\Drivers
 * @property CassandraSession $instance Instance of driver service
 * @property Config $config Config object
 * @method Config getConfig() Return the config object
 */
class Driver implements ExtendedCacheItemPoolInterface, AggregatablePoolInterface
{
    protected const CASSANDRA_KEY_SPACE = 'phpfastcache';
    protected const CASSANDRA_TABLE = 'cacheItems';

    use DriverBaseTrait;

    /**
     * @return bool
     */
    public function driverCheck(): bool
    {
        return extension_loaded('Cassandra') && class_exists(Cassandra::class);
    }

    /**
     * @return string
     */
    public function getHelp(): string
    {
        return <<<HELP
<p>
To install the php Cassandra extension via Pecl:
<code>sudo pecl install cassandra</code>
More information on: https://github.com/datastax/php-driver
Please note that this repository only provide php stubs and C/C++ sources, it does NOT provide php client.
</p>
HELP;
    }

    /**
     * @return DriverStatistic
     * @throws Exception
     */
    public function getStats(): DriverStatistic
    {
        $result = $this->instance->execute(
            new Cassandra\SimpleStatement(
                sprintf(
                    'SELECT SUM(cache_length) as cache_size FROM %s.%s',
                    self::CASSANDRA_KEY_SPACE,
                    self::CASSANDRA_TABLE
                )
            )
        );

        return (new DriverStatistic())
            ->setSize($result->first()['cache_size'])
            ->setRawData([])
            ->setData(implode(', ', array_keys($this->itemInstances)))
            ->setInfo('The cache size represents only the cache data itself without counting data structures associated to the cache entries.');
    }

    /**
     * @return bool
     * @throws PhpfastcacheLogicException
     * @throws Exception
     */
    protected function driverConnect(): bool
    {
        if ($this->instance instanceof CassandraSession) {
            throw new PhpfastcacheLogicException('Already connected to Couchbase server');
        }

        $clientConfig = $this->getConfig();

        $clusterBuilder = Cassandra::cluster()
            ->withContactPoints($clientConfig->getHost())
            ->withPort($clientConfig->getPort());

        if (!empty($clientConfig->isSslEnabled())) {
            if (!empty($clientConfig->isSslVerify())) {
                $sslBuilder = Cassandra::ssl()->withVerifyFlags(Cassandra::VERIFY_PEER_CERT);
            } else {
                $sslBuilder = Cassandra::ssl()->withVerifyFlags(Cassandra::VERIFY_NONE);
            }

            $clusterBuilder->withSSL($sslBuilder->build());
        }

        $clusterBuilder->withConnectTimeout($clientConfig->getTimeout());

        if ($clientConfig->getUsername()) {
            $clusterBuilder->withCredentials($clientConfig->getUsername(), $clientConfig->getPassword());
        }

        $this->instance = $clusterBuilder->build()->connect();

        /**
         * In case of emergency:
         * $this->instance->execute(
         *      new Cassandra\SimpleStatement(\sprintf("DROP KEYSPACE %s;", self::CASSANDRA_KEY_SPACE))
         * );
         */

        $this->instance->execute(
            new Cassandra\SimpleStatement(
                sprintf(
                    "CREATE KEYSPACE IF NOT EXISTS %s WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };",
                    self::CASSANDRA_KEY_SPACE
                )
            )
        );
        $this->instance->execute(new Cassandra\SimpleStatement(sprintf('USE %s;', self::CASSANDRA_KEY_SPACE)));
        $this->instance->execute(
            new Cassandra\SimpleStatement(
                sprintf(
                    '
                CREATE TABLE IF NOT EXISTS %s (
                    cache_uuid uuid,
                    cache_id varchar,
                    cache_data text,
                    cache_creation_date timestamp,
                    cache_expiration_date timestamp,
                    cache_length int,
                    PRIMARY KEY (cache_id)
                );',
                    self::CASSANDRA_TABLE
                )
            )
        );

        return true;
    }

    /**
     * @param CacheItemInterface $item
     * @return null|array
     */
    protected function driverRead(CacheItemInterface $item)
    {
        try {
            $options = $this->getCompatibleExecutionOptionsArgument(
                [
                    'arguments' => ['cache_id' => $item->getKey()],
                    'page_size' => 1,
                ]
            );
            $query = sprintf(
                'SELECT cache_data FROM %s.%s WHERE cache_id = :cache_id;',
                self::CASSANDRA_KEY_SPACE,
                self::CASSANDRA_TABLE
            );
            $results = $this->instance->execute(new Cassandra\SimpleStatement($query), $options);

            if ($results instanceof Cassandra\Rows && $results->count() === 1) {
                return $this->decode($results->first()['cache_data']);
            }

            return null;
        } catch (Exception $e) {
            return null;
        }
    }

    /**
     * @param CacheItemInterface $item
     * @return bool
     * @throws PhpfastcacheInvalidArgumentException
     */
    protected function driverWrite(CacheItemInterface $item): bool
    {
        /**
         * Check for Cross-Driver type confusion
         */
        if ($item instanceof Item) {
            try {
                $cacheData = $this->encode($this->driverPreWrap($item));
                $options = $this->getCompatibleExecutionOptionsArgument(
                    [
                        'arguments' => [
                            'cache_uuid' => new Cassandra\Uuid(),
                            'cache_id' => $item->getKey(),
                            'cache_data' => $cacheData,
                            'cache_creation_date' => new Cassandra\Timestamp((new DateTime())->getTimestamp()),
                            'cache_expiration_date' => new Cassandra\Timestamp($item->getExpirationDate()->getTimestamp()),
                            'cache_length' => strlen($cacheData),
                        ],
                        'consistency' => Cassandra::CONSISTENCY_ALL,
                        'serial_consistency' => Cassandra::CONSISTENCY_SERIAL,
                    ]
                );

                $query = sprintf(
                    'INSERT INTO %s.%s
                    (
                      cache_uuid, 
                      cache_id, 
                      cache_data, 
                      cache_creation_date, 
                      cache_expiration_date,
                      cache_length
                    )
                  VALUES (:cache_uuid, :cache_id, :cache_data, :cache_creation_date, :cache_expiration_date, :cache_length);
            ',
                    self::CASSANDRA_KEY_SPACE,
                    self::CASSANDRA_TABLE
                );

                $result = $this->instance->execute(new Cassandra\SimpleStatement($query), $options);
                /**
                 * There's no real way atm
                 * to know if the item has
                 * been really upserted
                 */
                return $result instanceof Cassandra\Rows;
            } catch (InvalidArgumentException $e) {
                throw new PhpfastcacheInvalidArgumentException($e, 0, $e);
            }
        } else {
            throw new PhpfastcacheInvalidArgumentException('Cross-Driver type confusion detected');
        }
    }

    /********************
     *
     * PSR-6 Extended Methods
     *
     *******************/

    /**
     * @param CacheItemInterface $item
     * @return bool
     * @throws PhpfastcacheInvalidArgumentException
     */
    protected function driverDelete(CacheItemInterface $item): bool
    {
        /**
         * Check for Cross-Driver type confusion
         */
        if ($item instanceof Item) {
            try {
                $options = $this->getCompatibleExecutionOptionsArgument(
                    [
                        'arguments' => [
                            'cache_id' => $item->getKey(),
                        ],
                    ]
                );
                $result = $this->instance->execute(
                    new Cassandra\SimpleStatement(
                        sprintf(
                            'DELETE FROM %s.%s WHERE cache_id = :cache_id;',
                            self::CASSANDRA_KEY_SPACE,
                            self::CASSANDRA_TABLE
                        )
                    ),
                    $options
                );

                /**
                 * There's no real way atm
                 * to know if the item has
                 * been really deleted
                 */
                return $result instanceof Cassandra\Rows;
            } catch (Exception $e) {
                return false;
            }
        } else {
            throw new PhpfastcacheInvalidArgumentException('Cross-Driver type confusion detected');
        }
    }

    /**
     * @return bool
     */
    protected function driverClear(): bool
    {
        try {
            $this->instance->execute(
                new Cassandra\SimpleStatement(
                    sprintf(
                        'TRUNCATE %s.%s;',
                        self::CASSANDRA_KEY_SPACE,
                        self::CASSANDRA_TABLE
                    )
                )
            );

            return true;
        } catch (Exception $e) {
            return false;
        }
    }

    /**
     * @param array<string, mixed> $options
     * @return array<string, mixed>|Cassandra\ExecutionOptions
     */
    protected function getCompatibleExecutionOptionsArgument(array $options)
    {
        if ($this->getConfig()->isUseLegacyExecutionOptions()) {
            return new Cassandra\ExecutionOptions($options);
        }

        return $options;
    }
}
