<?php

declare(strict_types=1);

namespace FiloBlu\Refilo\Model\Adapter;

use FiloBlu\Refilo\Helper\Data as HelperData;
use FiloBlu\Refilo\Remote\Entity\EntityInterface;
use MongoDB\Collection;
use MongoDB\Driver\BulkWrite;
use MongoDB\Driver\Command;
use MongoDB\Driver\Cursor;
use MongoDB\Driver\Exception\Exception;
use MongoDB\Driver\Manager;

/**
 * Class Mongo
 * @package FiloBlu\Refilo\Model\Adapter
 */
class Mongo extends AbstractAdapter
{
    /**
     * @var HelperData
     */
    private $helperData;

    /**
     * Mongo constructor.
     * @param HelperData $helperData
     */
    public function __construct(
        HelperData $helperData
    )
    {
        $this->helperData = $helperData;
    }

    /**
     * @param $mongoDatabase
     * @param $collection
     * @param array $propertiesArray
     */
    public function deleteDocumentsByProperties($mongoDatabase, $collection, array $propertiesArray)
    {
        $bulk = new BulkWrite();
        $bulk->delete($propertiesArray);

        try {
            $this->connect()->executeBulkWrite(sprintf('%s.%s', $mongoDatabase, $collection), $bulk);
        } catch (Exception $e) {
            echo $e->getMessage();
        }
    }

    /**
     * @return \MongoDB\Driver\Manager
     */
    public function connect()
    {
        if (!$this->_connection) {
            $this->_connection = new Manager($this->helperData->getConfigValue('mongo/general/mongo_host'));
        }

        return $this->_connection;
    }

    /**
     * @param $collection
     * @param $mongoDatabase
     * @throws Exception
     */
    public function dropCollection($collection, $mongoDatabase)
    {
        $collectionExist = !empty($this->connect()->executeCommand($mongoDatabase, new Command(['listCollections' => 1, 'filter' => ['name' => $collection]]))->toArray());

        if ($collectionExist) {
            try {
                $this->_connection->executeCommand($mongoDatabase, new Command(['drop' => $collection]));
                if (php_sapi_name() === 'cli') {
                    printf("Dropped %s collection\n", $collection);
                }
            } catch (Exception $e) {
                echo $e->getMessage();
            }
        }
    }

    /**
     * @param $collection
     * @param $mongoDatabase
     * @throws Exception
     */
    public function flushCollection($collection, $mongoDatabase)
    {
        $collectionExist = !empty($this->connect()->executeCommand($mongoDatabase, new Command(['listCollections' => 1, 'filter' => ['name' => $collection]]))->toArray());

        if ($collectionExist) {
            try {
                $this->connect()->executeCommand($mongoDatabase, new Command(['drop' => $collection]));
                if (php_sapi_name() === 'cli') {
                    printf("All documents removed from %s collection\n", (string)($collection));
                }
            } catch (Exception $e) {
                echo $e->getMessage();
            }
        } else {
            try {
                $this->connect()->executeCommand($mongoDatabase, new Command(['create' => $collection]));
                if (php_sapi_name() === 'cli') {
                    printf("%s collection regenerated without documents\n", (string)($collection));
                }
            } catch (Exception $e) {
                echo $e->getMessage();
            }
        }
    }

    /**
     * @param $key
     * @param $objects
     * @param $collection
     * @param bool $insertMode
     * @param $mongoDatabase
     * @return mixed|void
     */
    public function update($key, $objects, $collection, $insertMode, $mongoDatabase)
    {
        $bulk = new BulkWrite();

        foreach ($objects as $object) {
            $objToUpdate = $object;

            if ($object instanceof EntityInterface) {
                $objToUpdate = $object->toArray();
            }

            if ($insertMode === false) {
                $objToUpdate = ['$set' => $objToUpdate];
            }

            $bulk->update(
                ['_id' => $this->getIdValue($object, $key)],
                $objToUpdate,
                ['multi' => false, 'upsert' => $insertMode]
            );

        }

        try {
            $result = $this->connect()->executeBulkWrite(sprintf('%s.%s', $mongoDatabase, $collection), $bulk);
            if (php_sapi_name() === 'cli') {
                printf("Inserted %d document(s)\n", $result->getInsertedCount());
                printf("Matched  %d document(s)\n", $result->getMatchedCount());
                printf("Updated  %d document(s)\n", $result->getModifiedCount());
                printf("Upserted %d document(s)\n", $result->getUpsertedCount());
                printf("Deleted  %d document(s)\n", $result->getDeletedCount());
            }
        } catch (Exception $e) {
            echo $e->getMessage();
        }
    }

    /**
     * @param $object
     * @param $key
     * @return mixed
     */
    protected function getIdValue($object, $key)
    {
        if (is_array($object)) {
            return $object[$key];
        }

        if ($object instanceof EntityInterface) {
            return $object->getId();
        }

        return $object->{$key};
    }

    /**
     * @param $fields
     * @param $collection
     * @param null $database
     * @return mixed
     */
    public function ensureIndex($fields, $collection, $database)
    {
        try {
            $indexes = [];

            foreach ($fields as $field) {
                $indexes[] = [
                    'name' => $field,
                    'key'  => [$field => 1],
                    'ns'   => "$database.$collection",
                ];
            }

            $command = new Command([
                'createIndexes' => $collection,
                'indexes'       => $indexes,
            ]);

            $this->connect()->executeCommand($database, $command);
            if (php_sapi_name() === 'cli') {
                printf("added Indexes for %s \n", "$database.$collection");
            }
        } catch (Exception $e) {
            echo $e->getMessage();
        }
    }

    /**
     * @param iterable $collections
     * @param $database
     */
    public function createWildIndex(iterable $collections, $database)
    {
        if (version_compare($this->getVersion(), '4.2.0', '>')) {

            foreach ($collections as $collection) {
                $mongoCollection = new Collection($this->connect(), $database, $collection);
                $mongoCollection->createIndex(['$**' => 1]);
            }

        }
    }

    /**
     * @return mixed
     */
    public function getVersion()
    {
        $this->connect();
        $database = $this->helperData->getConfigValue('mongo/general/mongo_general_database');
        $cursor = $this->_connection->executeCommand($database, new Command(['serverStatus' => true]));
        $infos = iterator_to_array($cursor);
        return $infos[0]->version;
    }

    /**
     * @param string $collection
     * @param array $ids
     * @return \MongoDB\Driver\Cursor
     */
    public function getByIds(string $collection, array $ids): Cursor
    {
        $connection = $this->connect();
        $database = $this->helperData->getConfigValue('mongo/general/mongo_general_database');
        $documentCollection = new Collection($connection, $database, $collection);
        return $documentCollection->find(['_id' => ['$in' => $ids]]);
    }

    /**
     * Iterate every document of a collection.
     *
     * Lets callers process only the documents that actually exist, instead of probing a
     * numeric id range one-by-one.
     *
     * @param string $collection
     * @return \MongoDB\Driver\Cursor
     */
    public function getAll(string $collection): Cursor
    {
        $connection = $this->connect();
        $database = $this->helperData->getConfigValue('mongo/general/mongo_general_database');
        $documentCollection = new Collection($connection, $database, $collection);
        return $documentCollection->find([]);
    }

    /**
     * @param string $collection
     * @param string $id
     * @return array|object|null
     */
    public function getById(string $collection, string $id)
    {
        $connection = $this->connect();
        $database = $this->helperData->getConfigValue('mongo/general/mongo_general_database');
        $documentCollection = new Collection($connection, $database, $collection);
        return $documentCollection->findOne(['_id' => $id]);
    }
}
