<?php

namespace apexl\entityCore;

use apexl\entityCore\interfaces\EntityInterface;
use apexl\entityCore\interfaces\EntityOperatorInterface;
use Exception;
use Stringable;
use function apexl\Io\config;

/**
 * @mixin EntityOperatorInterface
 */
abstract class Entity implements EntityInterface, Stringable
{
    /** @var bool  Flag the entity as changed. Allows us to only update
     *             when something has been changed - should reduce overhead
     */
    protected bool $entityChanged = false;
    protected array $data = [];

    /** @var bool[] */
    protected array $dirty = [];
    protected string $type = '';
    protected EntityOperatorInterface $operator;
    protected array $requiredData = [];
    protected array $missingData = [];
    protected array $sensitiveDataFields = [];

    /**
     * entityBase constructor.
     * @throws Exception
     */
    public function __construct()
    {
        //first lets see what we can get from config.
        //reference the config singleton
        $this->initiateEntity();
    }

    protected function initiateEntity(): static
    {
        if ($baseOperator = config('entity.operator')) {
            $this->setOperator(new $baseOperator(config('entity')));
        }

        return $this;
    }

    /**
     * Method to allow us to set certain data as 'sensitive'. This prevents the data being returned in the getData() method, but allows it to still be accessible directly.
     */
    public function setSensitiveData($field, $replace = false): static
    {
        if (is_array($field)) {
            $this->sensitiveDataFields = $replace ? $field : array_merge($this->sensitiveDataFields, $field);
        } else {
            $this->sensitiveDataFields[] = $field;
        }

        return $this;
    }

    /**
     * Method to get the names of the fields marked sensitive.
     * @return array
     */
    public function getSensitiveDataNames(): array
    {
        return $this->sensitiveDataFields;
    }

    /**
     * Function to get the full Data array for the entity
     */
    public function getData(): array|bool
    {
        $data = $this->data;
        if (!empty($this->sensitiveDataFields)) {
            foreach ($this->sensitiveDataFields as $field) {
                unset($data[$field]);
            }
        }

        return $data;
    }

    /**
     * Function to set the entity data array. Useful when checking against loaded entities or for a direct overwrite.
     * @TODO - Why are we converting the encoding?
     * @return $this
     */
    public function setData(array $entity): static
    {
        //loop over and clean utf8 data from the entity. We use this not array_map because utf8_encode converts NULL to blank string.
        $utf8Safe = [];
        if (!empty($entity)) {
            foreach ($entity as $key => $value) {
                $utf8Safe[$key] = is_string($value) ? mb_convert_encoding(
                    $value,
                    'UTF-8',
                    mb_detect_encoding($value)
                ) : $value;
            }
        }
        $this->data = empty($entity) ? [] : json_decode(json_encode($utf8Safe), true);

        return $this;
    }

    /**
     * Getter for entity changed state.
     */
    public function isChanged(): bool
    {
        return $this->entityChanged;
    }

    /**
     * Allow us toi reset the change state of an entity (on load / save for example)
     */
    public function resetChanged()
    {
        $this->entityChanged = false;
    }

    /**
     * Magic PHP __get function to get the required field from the data object, without having to specify it to the entity.
     * @param $name
     * @return bool|mixed
     */
    public function __get($name)
    {
        return $this->getField($name);
    }

    /**
     * Magic PHP __set function to set provided field value
     * @param $name
     * @param $value
     * @return entity
     */
    public function __set($name, $value)
    {
        return $this->setField($name, $value);
    }

    /**
     * Getter for individual entity field
     * @param $field
     * @return bool|mixed
     */
    public function getField($field): mixed
    {
        return $this->data[$field] ?? null;
    }

    /**
     * Setter for individual entity field
     * @param string $field
     * @param mixed $value
     * @return $this
     */
    public function setField($field, $value): static
    {
        if (($this->data[$field] ?? null) !== $value) {
            $this->markDirty($field);
        }
        $this->data[$field] = $value;

        return $this;
    }

    protected function markDirty(string $field): void
    {
        $this->entityChanged = true;
        $this->dirty[$field] = true;
    }

    public function store(): static
    {
        $this->operator->store($this->data);
        $this->resetDirty();

        return $this;
    }

    private function resetDirty(): void
    {
        $this->dirty = [];
    }

    /**
     * Magic PHP __call function. Used for all operations. the use of `...` allows us to pass as many args as the callable method expects.
     * @param $name
     * @param $arguments
     * @return mixed
     * @throws Exception
     */
    public function __call($name, $arguments)
    {
        $response = $this->operator->$name(...$arguments);

        if (is_array($response) && ($response['updateEntityData'] ?? null) === true) {
            $this->setData((array) $response['data'] ?: []);
        }

        return $response;
    }

    /**
     * Allow entities to have defined required fields.
     * @param $fields
     */
    public function setRequiredData($fields)
    {
        $this->requiredData = $fields;
    }

    /**
     * Allow the addition of single fields to the required array
     * @param $field
     */
    public function addRequiredData($field)
    {
        $this->requiredData[] = $field;
    }

    /**
     * If validation fails, allow us to get the data that was missing.
     * @return array
     */
    public function getMissingData(): array
    {
        return $this->missingData;
    }

    /**
     * Function to check if the entity is valid. I.E. it has all of its required fields.
     * @return bool
     */
    public function isValid(): bool
    {
        $valid = true;
        $this->missingData = []; //clear this data at the start of a validation check
        if (!empty($this->requiredData)) {
            foreach ($this->requiredData as $field) {
                if (isset($this->data[$field]) && !empty($this->data[$field])) {
                    $valid = true;
                } else {
                    $valid = false;
                    $this->missingData[] = $field;
                }
            }
        }

        return $valid;
    }

    /**
     * function to determine the entity Type used in request URL generation based in the current class type.
     */
    public function getType(): string
    {
        return $this->type;
    }

    /**
     * Function to convert the entity to a jsonString
     */
    public function __toString(): string
    {
        return (string) json_encode($this->data);
    }

    /**
     * Magic __isset function to allow us to use isset on $data values retrieved by __get
     * @param $name
     * @return bool
     */
    public function __isset($name)
    {
        return isset($this->data[$name]);
    }

    /**
     * Magic __unset function to allow us to unset $data values.
     * @param $name
     */
    public function __unset($name)
    {
        if (isset($this->data[$name])) {
            unset($this->data[$name]);
            $this->entityChanged = true;
        }
    }

    public function isDirty(string $field): bool
    {
        return $this->dirty[$field] ?? false;
    }

    protected function getOperator(): EntityOperatorInterface
    {
        return $this->operator;
    }

    /**
     * Method to force any provided operators to implement the EntityOperatorInterface interface.
     * @return $this
     */
    public function setOperator(EntityOperatorInterface $operator): static
    {
        $entityClass = get_called_class();
        $entityName = substr($entityClass, strrpos($entityClass, '\\') + 1);
        $entityConfigProperty = str_ireplace("entity", "", $entityName);
        if ($accessControl = config(sprintf('app.accessControl.%s', $entityConfigProperty))) {
            $operator->setAccessControlType($accessControl);
        }

        $this->operator = $operator;

        return $this;
    }
}
