<?php

namespace apexl\Io\includes;

use apexl\Config\Configuration as Config;
use apexl\entityCore\interfaces\entityInterface;
use apexl\Vault\enums\SortDirection;
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
{
    protected pathUtility $path;
    protected Config $config;
    protected Mailer $mailer;
    protected Output $output;

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

    /**
     * @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);
    }

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

        return new $entityFQNS();
    }

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

    /**
     * Generic entity data getter - single or multi record, allows for filtering (can be overridden
     * @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()]);
        }

        [$sortField, $sortDir] = array_pad($params['orderBy'] ?? [], 2, null);
        if ($sortField) {
            $sortDir = $sortDir ? SortDirection::get($sortDir) : SortDirection::ASC;
        }

        //get our data, check if we're paging data add any filters ETC
        $entityData = $entity->loadByPage(
            $params,
            $filters,
            $sortField ? [$sortField, $sortDir] : []
        );

        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.
                        $fieldKey = sprintf('field__%s', $field);
                        if (is_callable($callable = $structure['callables'][$fieldKey] ?? null)) {
                            $cleanRow[$field] = $callable($fieldKey, $entityID, $row);
                        } elseif (isset($row[$field])) {
                            $cleanRow[$field] = $row[$field];
                        }
                    }
                    if (isset($structure['callables'])) {
                        foreach ($structure['callables'] as $field => $callable) {
                            //skip and field callables
                            if (str_contains((string) $field, 'field__')) {
                                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 = [];
                if (is_array($entityData['data']) && $entityData['data'] !== []) {
                    foreach (array_keys(current($entityData['data'])) as $tableHead) {
                        $headers[$tableHead] = $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);
    }

    /**
     * Function to build filter conditions
     * @param $entity
     * @param $params
     * @param  array  $validFields
     */
    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) || str_contains((string) $param, '.')) {
                    foreach ($value as $col) {
                        $filters[$param][] = $col;
                    }
                }
            } elseif (str_contains((string) $param, '.')) {
                //assume if we have a . in the params, then we're referencing a joined table.
                $filters[$param] = [$param, $value];
            } elseif (in_array($param, $validFields)) {
                $filters[$param] = [$param, $value];
            }
        }

        return $filters;
    }

    /**
     * @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);
    }

    /**
     * Generic SetEntity method - used to handle normal Entity data storage, without having to duplicate code.
     * @param $args
     * @param  entityInterface  $entity
     * @param  array  $extraData
     * @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);
    }

    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(false);
        $entity->setData([]); //blank the data so we dont have extra fields to store.
        foreach ($fields as $field) {
            if ($field->Field == "created" && is_null($field->Default)) {
                $field->Default = time();
            }
            if ($field->Field == "created_by" && is_null($field->Default)) {
                $field->Default = 1;
            }
            if (isset($body->{$field->Field}) && !is_null($body->{$field->Field})) {
                $entity->{$field->Field} = $this->dataTransform($entity, $field->Field, $body->{$field->Field});
            } else {
                $entity->{$field->Field} = $originalData[$field->Field] ?? $field->Default;
            }
        }

        if ($entity->extends && isset($entity->extends->entity)) {
            $parentEntityName = $entity->extends->entity;
            $parentEntity = new $parentEntityName();
            $parentFields = $parentEntity->getTableColumns();
            foreach ($parentFields 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', $entity::class);
            $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;
        }
    }

    /**
     * Tansform a form submitted entity value, based on rules defined in customEntity.php
     * @param $entity
     * @param $fieldName
     * @param $value
     * @return mixed|string
     */
    protected function dataTransform($entity, $fieldName, $value)
    {
        $dataTransFormers = $entity->dataTransformers();

        if (is_array($dataTransFormers)) {
            foreach ($dataTransFormers as $transformer) {
                if (isset($transformer['type'])) {
                    switch ($transformer['type']) {
                        case 'date':
                            $date = \DateTime::createFromFormat($transformer['fromFormat'], $value);
                            if ($date) {
                                $value = $date->format($transformer['toFormat']);
                            }
                            break;
                        case 'override':
                            if ($value == $transformer['fromValue']) {
                                $value = $transformer['toValue'];
                            }
                    }
                }
            }
        }


        return $value;
    }

    /**
     * @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 $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', $entity::class);
        $this->output::addMetadata('core.controller.delete.entityId', 'entityId', $id);
        $this->output::addResponse($request);

        return System::asJson($response, []);
    }

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

    protected function instantiateEntityFromPath(): false|\apexl\Io\includes\Entity
    {
        $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', '', (string) $moduleRecord['namespace']);
            //instantiate entity from FQDN
            $eName .= 'Entity';

            return new $baseNamespace.'\\entities\\'.$eName();
        }

        return false;
    }
}
