<?php

namespace apexl\Io\includes;

use apexl\entityCore\Entity as BaseEntity;
use apexl\Io\exceptions\RecordNotFoundException;
use apexl\Io\includes\Exception\EntityNotFoundException;
use apexl\Io\modules\user\services\currentUser;
use apexl\Io\operators\entityDatabaseOperator;

class Entity extends BaseEntity
{
    public $primaryKey;
    //managing Data
    public $extends;
    protected $table;
    protected $dataFields = [];
    //optional mapping for human-readable field data.
    protected $readOnlyFields = [];

    //pager fields
    protected $safeFields;
    protected $fieldConfig = [];
    protected $defaultPagerLimit = 25;
    protected $currentPagerOffset = 0;
    /** @var System */
    protected $system;
    /** @var apexl\Io\modules\user\entities\userEntity */
    protected $currentUser;

    public function __construct($table = '', $primaryKey = 'id', $extends = null)
    {
        $this->table = $table;
        $this->primaryKey = $primaryKey;
        $this->system = System::getInstance();
        parent::__construct();
        $this->setOperator(new entityDatabaseOperator($this->table, $this->primaryKey));
        $this->setReadOnlyFields([
            'id',
            "created",
            "created_by",
            "modified",
            "modified_by",
        ]);
        $this->extends = $extends;
        $this->getFieldConfig();

        //If we have the user module, load the current user.
        //@todo change this to a generic authentication check, change user module to extend that auth
        $calledClass = get_called_class();
        if (class_exists(
                '\apexl\Io\modules\user\userModule'
            ) && $calledClass != 'apexl\Io\modules\user\entities\userEntity') {
            $user = currentUser::getCurrentUser();
            $this->currentUser = $user ?? null;
        }

        //we only want to run these if we're installed. //Hacky fix for now
        if ($this->system->isInstalled()) {
            if (!empty($table)) {
                $this->getEntityDataFields();
            }
        }
        //set default blank object for meta
        $this->data["_metadata"] = (object) [];
    }

    public function getFieldConfig()
    {
        $this->fieldConfig = array_merge(
            [
                "created" => [
                    'name' => "Created",
                    'display' => function ($created) {
                        if (is_numeric($created)) {
                            return $created ? date('d-m-Y H:i:s', $created) : 'Unknown';
                        } else {
                            return $created;
                        }
                    },
                ],
                "created_by" => [
                    'name' => "Created By",
                    'display' => function ($createdBy) {
                        if ($createdBy && class_exists('\apexl\Io\modules\user\userModule')) {
                            $user = new \apexl\Io\modules\user\entities\userEntity();
                            $user->load($createdBy);

                            return $user->getNiceName();
                        }

                        return $createdBy;
                    },
                ],
                "modified" => [
                    'name' => "Modified",
                    'display' => function ($modified) {
                        return $modified ? date('d-m-Y H:i:s', $modified) : 'Unknown';
                    },
                ],
                "modified_by" => [
                    'name' => "Modified By",
                    'display' => function ($modifiedBy) {
                        if ($modifiedBy && class_exists('\apexl\Io\modules\user\userModule')) {
                            $user = new \apexl\Io\modules\user\entities\userEntity();
                            $user->load($modifiedBy);

                            return $user->getNiceName();
                        }

                        return $modifiedBy;
                    },
                ],
            ],
            $this->fieldConfig()
        );

        return $this->fieldConfig;
    }

    /**
     * Allow entities to override this and specify a 'nice name'
     * @return string
     */
    public function getNiceName(): string
    {
        return $this->getEntityName();
    }

    public function getEntityName(): string
    {
        return (new \ReflectionClass($this))->getShortName();
    }

    protected function fieldConfig()
    {
        return [];
    }

    /**
     * Function to get the fields for the entity (from the primary table).
     * Allows us to check what data is storable and strip data that isn't.
     * Will only return fields not specified as 'read only' in the entity settings.
     * @see the $readOnlyFields property.
     */
    public function getEntityDataFields()
    {
        if (empty($this->dataFields)) {
            $this->dataFields = $this->operator->getTableColumns();
            foreach ($this->dataFields as $entityField) {
                if (!in_array($entityField->Field, $this->readOnlyFields)) {
                    $this->safeFields[] = $entityField;
                }
            }
        }

        return $this->safeFields;
    }

    public function getTableColumns(): array
    {
        return $this->operator->getTableColumns();
    }

    /**
     * @throws RecordNotFoundException
     */
    public static function fromPrimaryKey(mixed $primaryKey): static
    {

        return self::from((new self)->getPrimaryKey(), $primaryKey);
    }

    /**
     * @throws RecordNotFoundException
     */
    public static function from(string $key, mixed $value): static
    {
        $entity = new static();
        $entity->loadBy($key, $value);

        if (!$entity->{$entity->primaryKey}) {
            throw new RecordNotFoundException();
        }

        return $entity;
    }

    public function getPrimaryKey(): string
    {
        return $this->primaryKey;
    }

    public function getEntityType(): string
    {
        return __CLASS__;
    }

    /**
     * @return array|string|string[]
     */
    public function getEntityTrueName()
    {
        return str_replace('Entity', '', $this->getEntityName());
    }

    /**
     * Allow entities to override this and specify a 'nice name'
     * @return string
     */
    public function getTableName(): string
    {
        return $this->table;
    }

    /**
     * By default, we want to store the current data object
     * @return $this
     * @throws \Exception
     */
    public function store(): Entity
    {
        //make sure dataFields is set.
        $this->getEntityDataFields();
        $this->setStandardFields();

        parent::__call('store', [$this->data]);

        return $this;
    }

    public function setStandardFields()
    {
        //in order to prevent an infinite loop, we may not have set current user  by the point. So, try it now
        if (!$this->currentUser && class_exists('\apexl\Io\modules\user\userModule')) {
            $user = currentUser::getCurrentUser();
            $this->currentUser = $user ?? null;
        }

        //set created / modified stamps if the fields exist
        if (isset($this->dataFields['created']) && !isset($this->data['created'])) {
            $this->data['created'] = time();
        }

        if (isset($this->dataFields['created_by']) && !isset($this->data['created_by'])) {
            $this->data['created_by'] = $this->currentUser->id ?? 0;
        }

        if (isset($this->dataFields['modified'])) {
            $this->data['modified'] = time();
        }

        if (isset($this->dataFields['modified_by'])) {
            $this->data['modified_by'] = $this->currentUser->id ?? 0;
        }
    }

    /**
     * Method to delete attached entity (or provided id)
     * @param  null  $id
     * @return $this
     * @throws \Exception
     */
    public function delete($id = null): Entity
    {
        if ($id) {
            $this->{$this->primaryKey} = $id;
        }
        $delete = Hook::processHook('entityPreDelete', $this, $this->getEntityName());
        if ($delete) {
            parent::__call('delete', [$this->{$this->primaryKey}]);
        }
        Hook::processHook('entityPostDelete', $this, $this->getEntityName());
        //deleting? clear the data
        $this->data = [];

        return $this;
    }

    /**
     * Function to get all results, paged
     * @param  null  $params
     * @param  array  $filters
     * @param  array  $orderBy
     * @return array
     * @throws \Exception
     */
    public function loadByPage($params = null, $filters = [], $orderBy = []): array
    {
        $this->setPagerLimit(($params['limit'] ?? false));
        $this->setCurrentPagerOffset(($params['offset'] ?? 0));
        //if we have a page param set, calculate offset on the fly.
        if ($page = $params['page'] ?? false) {
            //if we have a page, assume we need a limit, if its 0 set a default.
            $this->defaultPagerLimit = $this->defaultPagerLimit === 0 ? 25 : $this->defaultPagerLimit;
            $this->currentPagerOffset = ($this->defaultPagerLimit * $page) - $this->defaultPagerLimit;
        }

        $totalResults = $this->totalEntities($filters);
        $totalPages = $this->defaultPagerLimit == 0 ? 1 : ceil(($totalResults / (int) $this->defaultPagerLimit));

        $data = $this->loadMultiple($filters, $orderBy, $this->defaultPagerLimit, $this->currentPagerOffset, false);

        return [
            'data' => $data,
            'totalData' => $totalResults,
            'totalPages' => $totalPages,
            'totalResults' => count($data),
        ];
    }

    public function setPagerLimit($limit)
    {
        $this->defaultPagerLimit = $limit;

        return $this;
    }

    public function setCurrentPagerOffset($offset): Entity
    {
        $this->currentPagerOffset = $offset;

        return $this;
    }

    /**
     * @param  array  $filters
     * @return array|mixed
     * @throws \Exception
     */
    public function totalEntities(array $filters = [])
    {
        return parent::totalEntities($filters);
    }

    /**
     * Entity load multiple
     * @param  array  $conditions
     * @param  array  $orderBy
     * @param  int|bool  $limit
     * @param  int|bool  $offset
     * @param  bool  $asEntities
     * @return array
     * @throws \Exception
     */
    public function loadMultiple(
        array $conditions = [],
        array $orderBy = [],
        $limit = null,
        $offset = null,
        bool $asEntities = true
    ): array {
        $loaded = [];
        $results = parent::loadMultiple($conditions, $orderBy, $limit, $offset);
        //make sure data is UTF8 Safe, construct entities if required (default)
        if (!empty($results)) {
            foreach ($results as $result) {
                $data = System::makeUTF8Safe($result);
                $entityType = get_called_class();
                //set entity data (prevents us from having to reload entities)
                $entity = new $entityType();
                //we setData here even if we dont ultimately want the entities,
                //so we can take advantage of the formatting fixes and allow for any custom setData functionality
                $entity->setData((array) $data);
                //build an array of loaded entities, or just get the data if that's what we want.
                $entity = Hook::processHook('entityPostLoadMultiple', $entity, $entity->getEntityName());
                //TN do not key by ID as we need to preserve order with a 0 keyed array. This is to allow JS consumers to maintain the order.
                if ($asEntities) {
                    $loaded[] = $entity;
                } else {
                    $loaded[] = $entity->getData();
                }
            }
        }

        return $loaded;
    }

    /**
     * Override entityCore\Entity:getData()
     * Force loaded values through __Get for processing.
     * @return array
     * @todo Caching!
     */
    public function getData($forDisplay = true): array|bool
    {
        $data = parent::getData();
        if ($forDisplay) {
            $usableData = [];
            //force the loaded data to be passed through __get
            foreach ($data as $field => $value) {
                $value = $this->hrData($field, $value);
                if ($value !== Utils::HIDE_FROM_DISPLAY) {
                    $usableData[$field] = $value;
                }
            }

            return $usableData;
        }

        return $data;
    }

    public function hrData($fieldName, $data)
    {
        if (isset($this->fieldConfig[$fieldName]) && isset($this->fieldConfig[$fieldName]['display'])) {
            return $this->fieldConfig[$fieldName]['display']($data, $this);
        }

        return $data;
    }

    /**
     * Add metadata to the object, this will not be stored in the database unless manually done
     * @param $name
     * @param $data
     * @return $this
     */
    public function addMetadata($name, $data): Entity
    {
        $this->data["_metadata"]->$name = $data;

        return $this;
    }

    /**
     * Get data from metadata store, if it exists. Null otherwise
     * @param $name
     * @return mixed
     */
    public function getMetadata($name)
    {
        return $this->data["_metadata"]->$name ?? null;
    }

    /**
     * Function to flag certain data fields as read only (data changed in these fields will not be stored against the original loaded entity)
     * @param  string  $field
     * @return Entity
     */
    public function addReadOnlyField(string $field): Entity
    {
        $this->readOnlyFields[] = $field;

        return $this;
    }

    public function getReadOnlyFields(): array
    {
        return $this->readOnlyFields;
    }

    /**
     * Function to flag certain data fields as read only (data changed in these fields will not be stored against the original loaded entity)
     * @param  array  $fields
     */
    public function setReadOnlyFields(array $fields)
    {
        $this->readOnlyFields = $fields;
    }

    public function hrName($fieldName)
    {
        if (isset($this->fieldConfig[$fieldName]) && isset($this->fieldConfig[$fieldName]['name'])) {
            return $this->fieldConfig[$fieldName]['name'];
        }

        return $fieldName;
    }

    public function __get($name)
    {
        return $this->getField($name);
    }

    public function dataTransformers()
    {
        return [];
    }
}
