<?php
namespace apexl\Io\includes;

use apexl\Config\Singleton;
use apexl\entityCore\interfaces\entityInterface;
use apexl\Io\services\Mailer;
use apexl\Io\services\Output;
use apexl\Io\services\pathUtility;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;

class Controller
{
    /** @var pathUtility */
    protected $path;
    /** @var Singleton */
    protected $config;
    /** @var Mailer */
    protected $mailer;
    /** @var Output */
    protected $output;

    public function __construct()
    {
        $this->path = System::getRegisteredService(pathUtility::class);
        $this->config = System::getRegisteredService(Singleton::class);
        $this->mailer = System::getRegisteredService(Mailer::class);
        $this->output = System::getRegisteredService(Output::class);
    }

    /**
     * @param Request $request
     * @param Response $response
     * @param $args
     * @return Response
     */
    public function get(Request $request, Response $response, $args){
        $entity = $this->instantiateEntityFromRoute($request);
        if(!$entity){
            $this->output::addResponse($request, [], FALSE);
            return $this->throw404($response);
        }
        return $this->getEntityData($request, $response, $args, $entity);
    }

    /**
     * @param Request $request
     * @param Response $response
     * @param $args
     * @return Response
     * @throws \Exception
     */
    public function put(Request $request, Response $response, $args){
        $entity = $this->instantiateEntityFromRoute($request);
        if(!$entity){
            $this->output::addResponse($request, [], FALSE);
            return $this->throw404($response);
        }
        return $this->setEntityData($request, $response, $args, $entity);
    }

    /**
     * @param Request $request
     * @param Response $response
     * @return Response
     * @throws \Exception
     */
    public function post(Request $request, Response $response){
        $entity = $this->instantiateEntityFromRoute($request);
        if(!$entity){
            $this->output::addResponse($request, [], FALSE);
            return $this->throw404($response);
        }
        $success = $this->storeEntityData($request, $entity);
        return System::asJson($response, [], $success ? 200 : 400);
    }

    /**
     * @param Request $request
     * @param Response $response
     * @param $args
     * @return Response
     * @throws \Exception
     */
    public function delete(Request $request, Response $response, $args){
        $entity = $this->instantiateEntityFromRoute($request);
        if(!$entity){
            return $this->throw404($response);
        }
        if ($id = $args[$entity->primaryKey] ?? FALSE) {
            $entity->load($id);
            if (!isset($entity->id)) {
                $this->output::addResponse($request, [], FALSE);
                return $this->throw404($response);
            } else {
                $entity->delete();
            }
        }
        $this->output::addMetadata('core.controller.delete.entityType', 'entityType', get_class($entity));
        $this->output::addMetadata('core.controller.delete.entityId', 'entityId', $id);
        $this->output::addResponse($request);
        return System::asJson($response, []);
    }

    /**
     * @param Request $request
     * @param Response $response
     * @param $args
     * @return Response
     */
    public function patch(Request $request, Response $response, $args){
        return $this->throw404($response);
    }

    protected function instantiateEntityFromRoute(Request $request){
        $entityFQNS = $this->path->getRouteArg($request, 'entity');
        return new $entityFQNS();
    }

    /**
     * @return false|Entity
     */
    protected function instantiateEntityFromPath(){
        $noPath = $this->path->getNumElements();
        $eName = $this->path->getLastItem();
        $module = $this->path->getPath(($noPath - 2));
        //if we have a numeric entity then assume we have a specified ID, so shift everything back one step.
        if(is_numeric($eName)){
            $eName = $this->path->getPath(($noPath - 2));
            $module = $this->path->getPath(($noPath - 3));
        }
        //get the module namespace from the database
        $moduleRecord = System::getInstalledModuleByName($module);
        if($namespace = $moduleRecord['namespace'] ?? FALSE){
            //strip the module itself from the end of the namespace. We're assuming all entities exist in a
            //directory called 'entities' at the same level as the module file.
            $baseNamespace = str_replace($module.'Module', '', $moduleRecord['namespace']);
            //instantiate entity from FQDN
            $eName .= 'Entity';
            return new $baseNamespace.'\\entities\\'.$eName();
        }
        return FALSE;
    }

    /**
     * Generic entity data getter - single or multi record, allows for filtering (can be overridden
     * @param Request $request
     * @param Response $response
     * @param $args
     * @param entityInterface $entity
     * @param array $params
     * @param array $structure
     * @return Response @see this->customFilters and paging
     */
    protected function getEntityData(Request $request, Response $response, $args, EntityInterface $entity, $params = [], $structure = []): Response
    {
        if(empty($params)) {
            $params = $request->getQueryParams();
        }
        $filters = $this->buildFilterConditions($entity, $params);

        if ($id = $args[$entity->primaryKey] ?? FALSE) {
            $entity->load($id);
            if (!isset($entity->id)) {
                return System::asJson($response, ['data' => []], 404);
            }
            return System::asJson($response, ['data' => $entity->getData()]);
        }
        //get our data, check if we're paging data add any filters ETC
        $entityData = $entity->loadByPage($params, $filters, $params['orderBy'] ?? []);

        if(!empty($structure)){
            if($structure['tableHeader'] ?? FALSE) {
                $entityData['tableHeader'] = $structure['tableHeader'];
            }
            if($structure['fields'] ?? FALSE) {
                $alteredRows = [];
                foreach ($entityData['data'] as $entityID => $row) {
                    $cleanRow = [];
                    //keep the key to preserve order
                    //loop over the row, only keep the fields listed, in order of the fields provided.
                    foreach ($structure['fields'] as $key => $field){
                        //run callables OR take the data.
                        if(isset($structure['callables']) && isset($structure['callables']['field__'.$field])){
                            $callable = $structure['callables']['field__'.$field][0];
                            $method = $structure['callables']['field__'.$field][1];
                            $cleanRow[$field] = $callable::$method('field__'.$field, $entityID, $row);
                        } else {
                            if (isset($row[$field])) {
                                $cleanRow[$field] = $row[$field];
                            }
                        }
                    }
                    if(isset($structure['callables'])){
                        foreach($structure['callables'] as $field => $callable){
                            //skip and field callables
                            if(strpos($field, 'field__') !== FALSE){
                                continue;
                            }
                            $callable = $structure['callables'][$field][0];
                            $method = $structure['callables'][$field][1];
                            $cleanRow = $callable::$method($cleanRow, $field, $row);
                        }
                    }
                    $alteredRows[$entityID] = $cleanRow;
                }
                $entityData['data'] = $alteredRows;
            }
        }

        //do we need this data as a table?
        $tabaliseData = isset($params['asTable']) && !empty($params['asTable']) ? $params['asTable'] : 0;
        if($tabaliseData){
            //if so, we need to change the return format.
            //@todo - im sure we can do this better
            $entityData['tableHeader'] = $entityData['tableHeader'] ?? (function($entityData, $entity){
                    $headers = [];
                    foreach(array_keys(current($entityData['data'])) as $tableHead){
                        $headers[] = $entity->hrName($tableHead);
                    }
                    return $headers;
                })($entityData, $entity);
            $entityData['rows'] = $entityData['data'];
            unset($entityData['data']);
        }

        $entityData = Hook::processHook('getEntityData', $entityData, $entity->getEntityName());

        //no table? just return the data.
        $this->output::addResponse($request);
        return System::asJson($response, $entityData);
    }

    protected function storeEntityData(Request $request, EntityInterface $entity, $extraData = []){
        $body = $request->getParsedBody();

        //we grab the table cols so we can check which data we can actually store.
        $fields = $entity->getTableColumns();
        $originalData = $entity->getData();
        $entity->setData([]); //blank the data so we dont have extra fields to store.
        foreach($fields as $field){
            $entity->{$field->Field} = $body->{$field->Field} ?? ($originalData[$field->Field] ?? $field->Default);
        }

        if($entity->isValid()){
            $entity->store();
            //set the id into the original data
            $originalData[$entity->primaryKey] = $entity->{$entity->primaryKey};
            //restore the data into the entity.
            $entity->setData($originalData);
            $this->output::addMetadata('core.controller.put-post.entityType', 'entityType', get_class($entity));
            $this->output::addMetadata('core.controller.put-post.entityId', 'entityId', $entity->{$entity->primaryKey});
            $this->output::addMessage($this->path->getRouteName($request).'.complete', 'success', $entity->getNiceName().' saved.');
            $this->output::addResponse($request, $entity->getData()); //added so we can hook into this elsewhere.
            return TRUE;
        } else {
            $this->output::addMessage($this->path->getRouteName($request).'.validation', 'error', 'Saving '.$entity->getNiceName().' failed. The following fields are missing: '.implode(', ', $entity->getMissingData()));
            $this->output::addResponse($request, [], FALSE); //added so we can hook into this elsewhere.
            return FALSE;
        }
    }

    /**
     * Generic SetEntity method - used to handle normal Entity data storage, without having to duplicate code.
     * @param Request $request
     * @param Response $response
     * @param $args
     * @param entityInterface $entity
     * @param array $extraData
     * @return Response
     * @throws \Exception
     */
    protected function setEntityData(Request $request, Response $response, $args, EntityInterface $entity, $extraData = []): Response
    {
        //we have an id? then this is an update, so load the entity first.
        if ($id = $args[$entity->primaryKey] ?? FALSE) {
            $entity->load($id);
            if (!isset($entity->id)) {
                return $this->throw404($response);
            }
        }
        $success = $this->storeEntityData($request, $entity, $extraData);
        return System::asJson($response, [], $success ? 200 : 400);
    }

    /**
     * Function to build filter conditions
     * @param $entity
     * @param $params
     * @param array $validFields
     * @return array
     */
    protected function buildFilterConditions($entity, $params, $validFields = []): array
    {
        $filters = [];
        $fields = $entity->getTableColumns();
        //make sure that
        foreach($fields as $field){
            $validFields[] = $field->Field;
        }
        foreach($params as $param => $value){
            if(is_array($value)){
                //Skip orderBy's
                if($param == "orderBy"){
                    continue;
                }
                //assume $col 0 is the field name.
                //assume if we have a '.' in the params, then we're referencing a joined table.
                if(in_array($value[0], $validFields) || strpos($param, '.') !== FALSE) {
                    foreach ($value as $col) {
                        $filters[$param][] = $col;
                    }
                }
            } else {
                //assume if we have a . in the params, then we're referencing a joined table.
                if(strpos($param, '.') !== FALSE){
                    $filters[$param] = [$param, $value];
                } else {
                    if (in_array($param, $validFields)) {
                        $filters[$param] = [$param, $value];
                    }
                }
            }
        }
        return $filters;
    }

    /**
     * @param Response $response
     * @return Response
     */
    protected function throw404(Response $response){
        return System::asJson($response, [], 404);
    }
}