<?php

namespace apexl\Io\operators;

use apexl\entityCore\interfaces\EntityOperatorInterface;
use apexl\Io\includes\Hook;
use apexl\Io\services\Database;
use apexl\Vault\Vault;

class entityDatabaseOperator implements EntityOperatorInterface
{
    /** @var Vault  */
    protected $vault;
    protected $primaryKey;
    protected $columns;

    protected $defaultOperator;
    protected $accessControlType = 'default';

    /**
     * entityDatabaseOperator constructor.
     * @param $table
     * @param string $primaryKey
     * @param string $operatorVault
     */
    public function __construct(protected $dbTable, $primaryKey = 'id', protected $operatorVault = '', protected $extends=null)
    {
        $this->vault = Vault::getInstance();
        $this->setPrimaryKey($primaryKey);
        $this->defaultOperator = $this->vault->getDefaultVault();
    }

    public function setAccessControlType($type)
    {
        $this->accessControlType = $type;
    }

    public function setOperatorVault($vaultName)
    {
        $this->vault->setDefaultVault($vaultName);
        return $this;
    }

    public function useOperatorVault($reset = false)
    {
        if ($this->operatorVault != '') {
            if ($reset) {
                $this->setOperatorVault($this->defaultOperator);
            } else {
                $this->setOperatorVault($this->operatorVault);
            }
        }
    }

    /**
     * Set the primary key field
     * @param $key
     * @return $this
     */
    public function setPrimaryKey($key)
    {
        $this->primaryKey = $key;
        return $this;
    }

    /**
     * Query for loading entity data by id
     * @param $id
     * @param bool $skipAccessCheck
     * @return array
     */
    public function load($id, $skipAccessCheck = false)
    {
        $this->useOperatorVault();
        $query = $this->genericQuery('entityLoad', $skipAccessCheck);
        $query->where($this->dbTable.'.'.$this->primaryKey, $id);
        $query = Hook::processHook('entityLoadQueryExecute', $query, $this->dbTable, $this->extends);
        $entity = $query->execute()->fetchAssoc();
        if (!$skipAccessCheck) {
            $entity = Hook::processHook('entityAccess', $entity, $this->dbTable);
        }

        $this->useOperatorVault(true);
        return ['updateEntityData' => true, 'data' => $entity];
    }

    /**
     * Query for loading entity data by id, but skipping all hook and access control calls.
     * @param $id
     * @return array
     */
    public function cleanLoad($id)
    {
        $this->useOperatorVault();
        $query = clone $this->vault->select($this->dbTable);
        $entity = $query->fields()->where($this->dbTable.'.'.$this->primaryKey, $id)->execute()->fetchAssoc();

        $this->useOperatorVault(true);
        return ['updateEntityData' => true, 'data' => $entity];
    }

    /**
     * Query to load all entity data
     * @param array $conditions
     * @param array $orderBy
     * @param false $limit
     * @param false $offset
     * @return mixed
     */
    public function loadMultiple($conditions = [], $orderBy = [], $limit = false, $offset = false)
    {
        $this->useOperatorVault();
        $query = $this->genericQuery('entityLoadMultiple');
        // Do access control pass

        foreach ($conditions as $condition) {
            $operator = $condition[2] ?? '=';
            $type = $condition[3] ?? 'AND';
            $postCondition = $condition[4] ?? '';
            $query->where($condition[0], $condition[1], $operator, $type, $postCondition);
        }
        $query = Hook::processHook('entityLoadMultipleQueryExecute', $query, $this->dbTable, $this->extends);
        if (!empty($orderBy)) {
            if (!is_array($orderBy)) {
                //convert to an array
                $temp = $orderBy;
                $orderBy = [];
                $orderBy[0] = $temp;
            }
            for ($i=0; $i<12; $i+=2) { // Allow multiple Order By clauses
                if (isset($orderBy[$i]) && isset($orderBy[$i+1])) {
                    $direction = $orderBy[$i+1] ?? 'DESC';
                    $query->orderBy($orderBy[$i], $direction);
                }
            }
        }
        if ($limit) {
            $query->limit($limit, $offset);
        }

        $results = $query->execute()->fetchAll();
        $this->useOperatorVault(true);
        return $results;
    }

    /**
     * Function to return the total number of entity records available
     * @param array $conditions
     * @return mixed
     */
    public function totalEntities($conditions = [])
    {
        $this->useOperatorVault();
        $query = $this->vault->select($this->dbTable)->fields('count('.$this->dbTable.'.'.$this->primaryKey.') AS count');
        $query = Hook::processHook('accessControlJoins', $query, $this->accessControlType, $this->dbTable);
        $query = Hook::processHook('permissionRestrictViewEntities', $query, $this->dbTable, $this->getCurrentEntityName());
        $query = Hook::processHook('entityLoadMultipleQuery', $query, $this->dbTable, $this->extends, true);
        $query = Hook::processHook('accessControlClauses', $query, $this->accessControlType, $this->dbTable);
        if (!empty($conditions)) {
            foreach ($conditions as $condition) {
                $operator = $condition[2] ?? '=';
                $type = $condition[3] ?? 'AND';
                $postCondition = $condition[4] ?? '';
                $query->where($condition[0], $condition[1], $operator, $type, $postCondition);
            }
        }
        $query = Hook::processHook('entityLoadMultipleQueryExecute', $query, $this->dbTable, $this->extends, true);
        $result = $query->execute()->fetchAssoc();
        $this->useOperatorVault(true);
        return $result['count'];
    }

    /**
     * Function to store entity data - Accounts for metadata storage on the entity
     * @param $data
     * @return array|bool
     * @throws \Exception
     */
    public function store($data)
    {
        $this->useOperatorVault();
        $meta = null;
        //first, are we inserting or updating
        if (empty($data)) {
            throw new \Exception("Cannot store empty entity data");
        }
        //store and strip metadata if its available. Allows entities to contain processing and maintain easy store methods.
        if (isset($data['_metadata'])) {
            //we need to store this for update methods
            $meta = $data['_metadata'];
            unset($data['_metadata']);
        }
        //check for any extra fields that we don't have in the schema
        //make sure we know what the table supports
        $this->getTableColumns();
        $extraStore = [];
        foreach ($data as $field => $value) {
            if (!isset($this->columns[$field])) {
                $extraStore[$field] = $value;
                //remove it from data for now.
                unset($data[$field]);
            } elseif (is_bool($value)) {
                //add a bool check as we're already looping.
                //allow us to reset to bool after, in case this is important.
                //allow us to reset to bool after, in case this is important.
                //allow us to reset to bool after, in case this is important.
                //allow us to reset to bool after, in case this is important.
                $extraStore[$field] = $value;
                $data[$field] = (int)$value;
            }
        }

        $entity = [];
        if (isset($data[$this->primaryKey]) && !empty($data[$this->primaryKey])) {
            //we're storing a record, skip the access check to prevent data store issues.
            $entity = $this->cleanLoad($data[$this->primaryKey]);
        }
        if (isset($entity['data']) && !empty($entity['data'])) {
            //not empty? we have a record, so update.
            $data = Hook::processHook('preEntityUpdate', $data, $this->dbTable, $extraStore, $this->extends);
            $this->useOperatorVault();
            $this->vault->update($this->dbTable)->fields($data)->where($this->primaryKey, $data[$this->primaryKey])->execute();
            $this->useOperatorVault(true);
            $data = Hook::processHook('postEntityUpdate', $data, $this->dbTable, $data[$this->primaryKey]);
            return true;
        } else {
            $data = Hook::processHook('preEntityInsert', $data, $this->dbTable, $extraStore, $this->extends);
            $this->useOperatorVault();
            //we're inserting to force an entity update in case we have a new primary key.
            $this->vault->insert($this->dbTable)->fields($data)->execute();
            $lastStored = $this->getLastNewRecord(true);
            $this->useOperatorVault(true);

            //force the local entity to be updated with the new data (id etc)
            $updatedData = $this->load($lastStored[$this->primaryKey], true)['data'];
            if ($meta) {
                $updatedData['_metadata'] = $meta;
            }
            //re-add any extra fields for use by other modules.
            if ($extraStore !== []) {
                foreach ($extraStore as $field => $value) {
                    $data[$field] = $value;
                }
            }
            $this->useOperatorVault(true);
            $data = Hook::processHook('postEntityInsert', $data, $this->dbTable, $lastStored[$this->primaryKey]);
            return ['updateEntityData' => true, 'data' => $updatedData];
        }
    }

    /**
     * Function to get the last created record for the given entity (by DB table)
     * @param bool $skipAccess
     * @return mixed
     */
    public function getLastNewRecord($skipAccess = false)
    {
        $this->useOperatorVault();
        $query = $this->genericQuery();
        $result = $query->orderBy($this->primaryKey, 'DESC')->execute()->fetchAssoc();
        if (!$skipAccess) {
            $result = Hook::processHook('entityAccess', $result, $this->dbTable);
        }
        $this->useOperatorVault(true);
        return $result;
    }

    /**
     * Function to delete data by ID
     * @param $id
     * @return mixed
     */
    public function delete($id)
    {
        $this->useOperatorVault();
        $deleted = $this->vault->delete($this->dbTable)->where($this->primaryKey, $id)->execute();
        $this->useOperatorVault(true);
        return $deleted;
    }

    /**
     * Function to load all data available
     * @return mixed
     */
    public function loadAll()
    {
        $this->useOperatorVault();
        $query = $this->vault->select($this->dbTable);
        $result = $query->execute()->fetchAll();
        $result =  Hook::processHook('entityAccess', $result, $this->dbTable);
        $this->useOperatorVault(true);
        return $result;
    }

    /**
     * Describe method to generate known  table columns for validation and safe data storage
     * @return mixed
     */
    public function getTableColumns()
    {
        $this->useOperatorVault();
        if (!$this->columns) {
            $columns = $this->describe();
            //build the data column.
            foreach ($columns as $col) {
                $this->columns[$col->Field] = $col;
            }
        }
        $this->useOperatorVault(true);
        return $this->columns;
    }

    protected function describe()
    {
        return Database::describe($this->dbTable);
    }

    protected function genericQuery($hook = 'entityLoad', $skipAccessCheck = false)
    {
        //clone here to prevent access checks interfering.
        $query = clone $this->vault->select($this->dbTable);
        $queryFields = Hook::processHook($hook.'Fields', [$this->dbTable => ['*']], $this->dbTable, $this->extends);
        $fields = [];
        foreach ($queryFields as $table => $queryField) {
            foreach ($queryField as $tableColumn) {
                $fields[] = str_contains((string) $tableColumn, '.') ? $tableColumn : $table.'.'.$tableColumn;
            }
        }
        $query->fields($fields);
        if (!$skipAccessCheck) {
            $query = Hook::processHook('accessControlJoins', $query, $this->accessControlType, $this->dbTable);
        }
        $query = Hook::processHook('permissionRestrictViewEntities', $query, $this->dbTable, $this->getCurrentEntityName());
        $query = Hook::processHook($hook.'Query', $query, $this->dbTable, $this->extends);
        if (!$skipAccessCheck) {
            $query = Hook::processHook('accessControlClauses', $query, $this->accessControlType, $this->dbTable);
        }

        return $query;
    }

    protected function getCurrentEntityName()
    {
        $instantiatedClass = get_called_class();
        return str_ireplace("operator", "", substr($instantiatedClass, strrpos($instantiatedClass, '\\') + 1));
    }
}
