<?php

namespace BaoPham\DynamoDb;

use Exception;
use DateTime;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;

/**
 * Class DynamoDbModel.
 */
abstract class DynamoDbModel extends Model
{
    /**
     * Always set this to false since DynamoDb does not support incremental Id.
     *
     * @var bool
     */
    public $incrementing = false;

    /**
     * @var \BaoPham\DynamoDb\DynamoDbClientInterface
     */
    protected static $dynamoDb;

    /**
     * @deprecated
     * @var \Aws\DynamoDb\Marshaler
     */
    protected $marshaler;

    /**
     * @deprecated
     * @var \BaoPham\DynamoDb\EmptyAttributeFilter
     */
    protected $attributeFilter;

    /**
     * Indexes.
     *   [
     *     '<simple_index_name>' => [
     *          'hash' => '<index_key>'
     *     ],
     *     '<composite_index_name>' => [
     *          'hash' => '<index_hash_key>',
     *          'range' => '<index_range_key>'
     *     ],
     *   ]
     *
     * @var array
     */
    protected $dynamoDbIndexKeys = [];

    /**
     * Array of your composite key.
     * ['<hash>', '<range>']
     *
     * @var array
     */
    protected $compositeKey = [];

    /**
     * Default Date format
     * ISO 8601 Compliant
     */
    protected $dateFormat = DateTime::ATOM;


    public function __construct(array $attributes = [])
    {
        $this->bootIfNotBooted();

        $this->syncOriginal();

        $this->fill($attributes);

        $this->setupDynamoDb();
    }

    /**
     * Get the DynamoDbClient service that is being used by the models.
     *
     * @return DynamoDbClientInterface
     */
    public static function getDynamoDbClientService()
    {
        return static::$dynamoDb;
    }

    /**
     * Set the DynamoDbClient used by models.
     *
     * @param DynamoDbClientInterface $dynamoDb
     *
     * @return void
     */
    public static function setDynamoDbClientService(DynamoDbClientInterface $dynamoDb)
    {
        static::$dynamoDb = $dynamoDb;
    }

    /**
     * Unset the DynamoDbClient service for models.
     *
     * @return void
     */
    public static function unsetDynamoDbClientService()
    {
        static::$dynamoDb = null;
    }

    protected function setupDynamoDb()
    {
        $this->marshaler = static::$dynamoDb->getMarshaler();
        $this->attributeFilter = static::$dynamoDb->getAttributeFilter();
    }

    public function newCollection(array $models = [], $index = null)
    {
        return new DynamoDbCollection($models, $index);
    }

    public function save(array $options = [])
    {
        $create = !$this->exists;

        if ($this->fireModelEvent('saving') === false) {
            return false;
        }

        if ($create && $this->fireModelEvent('creating')  === false) {
            return false;
        }

        if (!$create && $this->fireModelEvent('updating') === false) {
            return false;
        }

        if ($this->usesTimestamps()) {
            $this->updateTimestamps();
        }

        $saved = $this->newQuery()->save();

        if (!$saved) {
            return $saved;
        }

        $this->exists = true;
        $this->wasRecentlyCreated = $create;
        $this->fireModelEvent($create ? 'created' : 'updated', false);

        $this->finishSave($options);

        return $saved;
    }

    /**
     * Saves the model to DynamoDb asynchronously and returns a promise
     * @param array $options
     * @return bool|\GuzzleHttp\Promise\Promise
     */
    public function saveAsync(array $options = [])
    {
        $create = !$this->exists;

        if ($this->fireModelEvent('saving') === false) {
            return false;
        }

        if ($create && $this->fireModelEvent('creating')  === false) {
            return false;
        }

        if (!$create && $this->fireModelEvent('updating') === false) {
            return false;
        }

        if ($this->usesTimestamps()) {
            $this->updateTimestamps();
        }

        $savePromise = $this->newQuery()->saveAsync();

        $savePromise->then(function ($result) use ($create, $options) {
            if (Arr::get($result, '@metadata.statusCode') === 200) {
                $this->exists = true;
                $this->wasRecentlyCreated = $create;
                $this->fireModelEvent($create ? 'created' : 'updated', false);

                $this->finishSave($options);
            }
        });

        return $savePromise;
    }

    public function update(array $attributes = [], array $options = [])
    {
        return $this->fill($attributes)->save();
    }

    public function updateAsync(array $attributes = [], array $options = [])
    {
        return $this->fill($attributes)->saveAsync($options);
    }

    public static function create(array $attributes = [])
    {
        $model = new static;

        $model->fill($attributes)->save();

        return $model;
    }

    public function delete()
    {
        if (is_null($this->getKeyName())) {
            throw new Exception('No primary key defined on model.');
        }

        if ($this->exists) {
            if ($this->fireModelEvent('deleting') === false) {
                return false;
            }

            $this->exists = false;

            $success = $this->newQuery()->delete();

            if ($success) {
                $this->fireModelEvent('deleted', false);
            }

            return $success;
        }
    }

    public function deleteAsync()
    {
        if (is_null($this->getKeyName())) {
            throw new Exception('No primary key defined on model.');
        }

        if ($this->exists) {
            if ($this->fireModelEvent('deleting') === false) {
                return false;
            }

            $this->exists = false;

            $deletePromise = $this->newQuery()->deleteAsync();

            $deletePromise->then(function () {
                $this->fireModelEvent('deleted', false);
            });

            return $deletePromise;
        }
    }

    public static function all($columns = [])
    {
        $instance = new static;

        return $instance->newQuery()->get($columns);
    }

    public function refresh()
    {
        if (! $this->exists) {
            return $this;
        }

        $query = $this->newQuery();

        $refreshed = $query->find($this->getKeys());

        $this->setRawAttributes($refreshed->toArray());

        return $this;
    }

    /**
     * @return DynamoDbQueryBuilder
     */
    public function newQuery()
    {
        $builder = new DynamoDbQueryBuilder($this);

        foreach ($this->getGlobalScopes() as $identifier => $scope) {
            $builder->withGlobalScope($identifier, $scope);
        }

        return $builder;
    }

    public function hasCompositeKey()
    {
        return !empty($this->compositeKey);
    }

    /**
     * @deprecated
     * @param $item
     * @return array
     */
    public function marshalItem($item)
    {
        return $this->marshaler->marshalItem($item);
    }

    /**
     * @deprecated
     * @param $value
     * @return array
     */
    public function marshalValue($value)
    {
        return $this->marshaler->marshalValue($value);
    }

    /**
     * @deprecated
     * @param $item
     * @return array|\stdClass
     */
    public function unmarshalItem($item)
    {
        return $this->marshaler->unmarshalItem($item);
    }

    public function setId($id)
    {
        if (!is_array($id)) {
            $this->setAttribute($this->getKeyName(), $id);

            return $this;
        }

        foreach ($id as $keyName => $value) {
            $this->setAttribute($keyName, $value);
        }

        return $this;
    }

    /**
     * @return \Aws\DynamoDb\DynamoDbClient
     */
    public function getClient()
    {
        return static::$dynamoDb->getClient($this->getConnectionName());
    }

    /**
     * Get the value of the model's primary key.
     *
     * @return mixed
     */
    public function getKey()
    {
        return $this->getAttribute($this->getKeyName());
    }

    /**
     * Get the value of the model's primary / composite key.
     * Use this if you always want the key values in associative array form.
     *
     * @return array
     *
     * ['id' => 'foo']
     *
     * or
     *
     * ['id' => 'foo', 'id2' => 'bar']
     */
    public function getKeys()
    {
        if ($this->hasCompositeKey()) {
            $key = [];

            foreach ($this->compositeKey as $name) {
                $key[$name] = $this->getAttribute($name);
            }

            return $key;
        }

        $name = $this->getKeyName();

        return [$name => $this->getAttribute($name)];
    }

    /**
     * Get the primary key for the model.
     *
     * @return string
     */
    public function getKeyName()
    {
        return $this->primaryKey;
    }

    /**
     * Get the primary/composite key for the model.
     *
     * @return array
     */
    public function getKeyNames()
    {
        return $this->hasCompositeKey() ? $this->compositeKey : [$this->primaryKey];
    }

    /**
     * @return array
     */
    public function getDynamoDbIndexKeys()
    {
        return $this->dynamoDbIndexKeys;
    }

    /**
     * @param array $dynamoDbIndexKeys
     */
    public function setDynamoDbIndexKeys($dynamoDbIndexKeys)
    {
        $this->dynamoDbIndexKeys = $dynamoDbIndexKeys;
    }

    /**
     * @deprecated
     * @return \Aws\DynamoDb\Marshaler
     */
    public function getMarshaler()
    {
        return $this->marshaler;
    }

    /**
     * Remove non-serializable properties when serializing.
     *
     * @return array
     */
    public function __sleep()
    {
        return array_keys(
            Arr::except(get_object_vars($this), ['marshaler', 'attributeFilter'])
        );
    }

    /**
     * When a model is being unserialized, check if it needs to be booted and setup DynamoDB.
     *
     * @return void
     */
    public function __wakeup()
    {
        parent::__wakeup();
        $this->setupDynamoDb();
    }
}
