<?php

namespace App\Helpers\ThirdParty\DFS;

use DFSClientV3\Bootstrap\Application;
use DFSClientV3\Models\DataMapper;
use DFSClientV3\Services\HttpClient\Handlers\Responses;
use DFSClientV3\Services\HttpClient\HttpClient;

abstract class AbstractModel
{
    /**
     * @var null|int timeout for http request
     */
    protected $timeOut = null;

    protected $apiVersion = null;

    /**
     * @var string | null url to DataForSeo API
     */
    protected $url = null;

    /**
     * @var int|null PostId is needed for a request, for more information check an example or DFSApi
     */
    protected $postId = null;

    /**
     * @var null This field is not working at the moment
     */
    public $statusCode;

    /**
     * @var null This field is not working at the moment
     */
    public $statusMessage;

    /**
     * @var array
     */
    protected $headers = [];

    /**
     * @var null This field is not working at the moment
     */
    protected $requiredField;

    /**
     * @var null This field is not working at the moment
     */
    protected $mainData;

    /**
     * @var static RequestType, this param will be sent to DFS API, .xml, .gzip etc...
     */
    protected $requestType;

    /**
     * @var string Method for http request. POST, GET , PUT, DELETE
     */
    protected $method = 'POST';

    /**
     * This variable is required for all extended Classes from AbstractModel.
     *
     * @var null|string It is a name, when you want to send a request to api, use this param as an api endpoint, example: cmn_user
     */
    protected $requestToFunction;

    /**
     * This variable is required for all extended Classes from AbstractModel
     * example for path: results->0->related.
     *
     * results - is object link from DFSResponse
     * 0       - is Index of array or postID
     * related - is object link containing main data
     *
     * @var null|string It is a system variable, it contains a path to main data from response and creates iterable(IteratorAggregator) response.
     */
    protected $pathToMainData = null;

    /**
     * @var bool If payload contains postId
     */
    protected $isSupportedMerge = false;

    /**
     * @var null|string
     */
    private $DFSLogin = null;

    /**
     * @var null|string
     */
    private $DFSPassword = null;

    public $response;

    protected $application;

    protected $config;

    /**
     * @var array
     */
    protected $payload;

    protected $isProcessed;

    protected $mappedMainModel;

    protected $seTypes = ['organic', 'maps', 'local_finder', 'news', 'images', 'search_by_image'];

    /**
     * @var bool
     */
    protected $jsonContainVariadicType = false;

    /**
     * @var array
     */
    protected $pathsToVariadicTypesAndValue = [];

    /**
     * @var array
     */
    protected $customFunctionForPaths = [];

    /**
     * @var array
     */
    protected $pathsToDictionary = [];

    /**
     * @var bool Temp variable, for detect when use new mapper
     */
    protected $useNewMapper = false;

    /**
     * new version of DataForSeo has two variations of result
     * 1. Object contains other objects for response.
     * 2. Object contains other objects, but they is iterable.
     *
     * if model has property $resultShouldBeTransformedToArray as true, result index will be transformed to array
     *
     * @var bool
     */
    protected $resultShouldBeTransformedToArray = false;

    public function __construct()
    {
        $this->application = Application::getInstance();
        $this->config = $this->application->getConfig();
        $this->initDefaultMethods();

    }

    /**
     * @param $postID mixed
     */
    public function setPostID($postID)
    {
        $this->postId = $postID;

        return $this;
    }

    /**
     * @param $headers array
     */
    public function setHeaders($headers)
    {
        if (count($headers) > 0) {
            foreach ($headers as $key => $value) {
                $this->config['headers'][$key] = $value;
            }
        }

        return $this;
    }

    /**
     * This method will run request to api.
     */
    protected function get()
    {
        $response = $this->process();
        // check if response contain valid json

        $validResponse = json_decode($response->getResponse());

        if (json_last_error() !== JSON_ERROR_NONE) {
            $validResponse = ['status_code' => 50000, 'status_message' => 'error.'];
        }

        return $this->mapData(json_encode($validResponse), $response->getStatus());
    }

    /**
     * @return mixed
     *
     * @throws \Exception
     */
    public static function getAfterMerge(array $modelPool)
    {
        if (empty($modelPool)) {
            throw new \Exception('Model pool can not be empty');
        }

        $payload = [];
        $url = null;
        $apiVersion = null;
        $timeOut = null;
        $login = null;
        $password = null;
        $method = null;
        $requestToFunction = null;
        $config = Application::getInstance()->getConfig();
        $pathToMainData = null;
        $resultShouldTransformedToArray = false;
        $useNewMapper = false;
        $isJsonContainVariadicType = false;
        $pathsToVariadicTypesAndValue = [];
        $customFunctionForPaths = [];

        foreach ($modelPool as $key => $model) {

            // kostyl, all variable will be rewrite every iteration
            $url = $model->url;
            $apiVersion = $model->apiVersion;
            $timeOut = $model->timeOut;
            $login = $model->DFSLogin;
            $password = $model->DFSPassword;
            $method = $model->method;
            $requestToFunction = $model->requestToFunction;
            $pathToMainData = $model->pathToMainData;
            $resultShouldTransformedToArray = $model->resultShouldBeTransformedToArray;
            $useNewMapper = $model->isUseNewMapper();
            $isJsonContainVariadicType = $model->isJsonContainVariadicType();
            $pathsToVariadicTypesAndValue = $model->getPathsToVariadicTypesAndValue();
            $customFunctionForPaths = $model->getCustomFunctionForPaths();

            if ($model->isSupportedMerge) {
                if ($model->postId === null) {
                    $payload['json'][] = $model->payload;
                }

                if ($model->postId !== null) {
                    $payload['json'][$model->postId] = $model->payload;
                }
            } else {
                throw new \Exception('Provided model '.get_class($model).' is not supported merge');
            }
        }

        $payload['headers'] = $config['headers'];

        $http = new HttpClient($url, $apiVersion, $timeOut, $login, $password);

        $res = $http->sendSingleRequest($method, $requestToFunction, $payload);
        // check if response contain valid json

        $validResponse = json_decode($res->getResponse());

        if (json_last_error() !== JSON_ERROR_NONE) {
            $validResponse = ['status_code' => 50000, 'status_message' => 'error.'];
        }

        //get called class.
        $calledClassNameWithNapeSpace = get_called_class();
        $classNameArray = explode('\\', $calledClassNameWithNapeSpace);
        //for php 7.3 can be use array_last_key
        $classNameArray[count($classNameArray) - 1];

        $mapper = new DataMapper($classNameArray[count($classNameArray) - 1], $res->getStatus(), $pathToMainData);

        if ($useNewMapper) {
            $paveDataOptions = new PaveDataOptions();
            $paveDataOptions->setJson(json_encode($validResponse));
            $paveDataOptions->setJsonContainVariadicType($isJsonContainVariadicType);
            $paveDataOptions->setPathsToVariadicTypesAndValue($pathsToVariadicTypesAndValue);
            $paveDataOptions->setCustomFunctionForPaths($customFunctionForPaths);

            $mappedModel = $mapper->paveDataNew($paveDataOptions);

            return $mappedModel;
        }

        $mappedModel = $mapper->paveData(json_encode($validResponse), null, $resultShouldTransformedToArray);

        return $mappedModel;

    }

    /**
     * @param  array|AbstractModel[]  $modelPool
     * @return array|null
     */
    public static function getAsync(array $modelPool, int $timeout = 100)
    {

        if (count($modelPool) > 100) {
            return null;
        }

        $requestsPool = [];
        $results = [];

        $payload = [];
        $url = null;
        $apiVersion = null;
        $timeOut = null;
        $login = null;
        $password = null;

        $config = Application::getInstance()->getConfig();
        $pathToMainData = null;
        $resultShouldTransformedToArray = false;
        $useNewMapper = false;
        $isJsonContainVariadicType = false;
        $pathsToVariadicTypesAndValue = [];
        $customFunctionForPaths = [];
        $pathsToDictionary = [];

        foreach ($modelPool as $key => $model) {

            // kostyl, all variable will be rewrite every iteration
            $url = $model->url;
            $apiVersion = $model->apiVersion;
            $login = $model->DFSLogin;
            $password = $model->DFSPassword;

            $pathToMainData = $model->pathToMainData;
            $resultShouldTransformedToArray = $model->resultShouldBeTransformedToArray;
            $useNewMapper = $model->isUseNewMapper();
            $isJsonContainVariadicType = $model->isJsonContainVariadicType();
            $pathsToVariadicTypesAndValue = $model->getPathsToVariadicTypesAndValue();
            $customFunctionForPaths = $model->getCustomFunctionForPaths();
            $pathsToDictionary = $model->getPathsToDictionary();

            $payload['json'][] = $model->getPayload();
            $payload['headers'] = $config['headers'];

            $requestsPool[$key]['method'] = $model->getHttpMethod();
            $requestsPool[$key]['url'] = $model->getRequestToFunction();
            $requestsPool[$key]['params'] = $payload;
            $requestsPool[$key]['pathToMainData'] = $model->getPathToMainData();
        }

        $http = new HttpClient($url, $apiVersion, $timeout, $login, $password);
        $responses = $http->sendAsyncRequests($requestsPool, null);

        foreach ($responses as $response) {

            /**
             * @var Responses $response
             */

            //get called class.
            $calledClassNameWithNapeSpace = get_called_class();
            $classNameArray = explode('\\', $calledClassNameWithNapeSpace);
            //for php 7.3 can be use array_last_key
            $classNameArray[count($classNameArray) - 1];

            $mapper = new DataMapper($classNameArray[count($classNameArray) - 1], $response->getStatus(), $pathToMainData);

            if ($useNewMapper) {
                $paveDataOptions = new PaveDataOptions();
                $paveDataOptions->setJson($response->getResponse());
                $paveDataOptions->setJsonContainVariadicType($isJsonContainVariadicType);
                $paveDataOptions->setPathsToDictionary($pathsToDictionary);
                $paveDataOptions->setPathsToVariadicTypesAndValue($pathsToVariadicTypesAndValue);
                $paveDataOptions->setCustomFunctionForPaths($customFunctionForPaths);

                $mappedModel = $mapper->paveDataNew($paveDataOptions);

                $results[] = $mappedModel;
            }

            if (! $useNewMapper) {
                $results[] = $mapper->paveData($response->getResponse(), null, $resultShouldTransformedToArray);
            }
        }

        return $results;
    }

    /**
     * @return \DFSClientV3\Services\HttpClient\Handlers\Responses
     */
    public function process()
    {
        $http = new HttpClient($this->url, $this->apiVersion, $this->timeOut, $this->DFSLogin, $this->DFSPassword);
        $payload = [];

        if (! $this->application) {
            dd('DFSClient was not init, add to your code: $DFSClient = new DFSClient() ');
        }

        if (! $this->requestToFunction) {
            dd('requestFunction can not be null, set this field in your model: '.get_called_class());
        }

        if ($this->postId === null) {
            $payload['json'][0] = $this->payload;
        }

        if ($this->postId != null) {
            $payload['json'][$this->postId] = $this->payload;
        }

        $payload['headers'] = $this->config['headers'];

        $res = $http->sendSingleRequest($this->method, $this->requestToFunction, $payload);

        return $res;
    }

    public function getAsJson()
    {
        $result = $this->process()->getResponse();

        return $result;
    }

    /**
     * @return string|null
     */
    public function getPathToMainData()
    {
        return $this->pathToMainData;
    }

    /**
     * @return mixed
     */
    public function getCalledClass()
    {
        $calledClassNameWithNapeSpace = get_called_class();
        $classNameArray = explode('\\', $calledClassNameWithNapeSpace);

        //for php 7.3 can be use array_last_key
        return $classNameArray[count($classNameArray) - 1];
    }

    public function setOpt()
    {

    }

    public function setLogin(string $newLogin)
    {
        $this->DFSLogin = $newLogin;

        return $this;
    }

    public function setPassword(string $newPassword)
    {
        $this->DFSPassword = $newPassword;

        return $this;
    }

    /**
     * @param  bool  $isSuccesful
     *
     * return mixed;
     */
    protected function mapData(string $json, bool $isSuccesful = false)
    {

        $mapper = new DataMapper($this->getCalledClass(), $isSuccesful, $this->pathToMainData);

        if ($this->useNewMapper) {
            $paveDataOptions = new PaveDataOptions();
            $paveDataOptions->setJson($json);
            $paveDataOptions->setJsonContainVariadicType($this->isJsonContainVariadicType());
            $paveDataOptions->setPathsToVariadicTypesAndValue($this->getPathsToVariadicTypesAndValue());
            $paveDataOptions->setPathsToDictionary($this->getPathsToDictionary());
            $paveDataOptions->setCustomFunctionForPaths($this->getCustomFunctionForPaths());

            $mappedModel = $mapper->paveDataNew($paveDataOptions);

            return $mappedModel;
        }

        $mappedModel = $mapper->paveData($json, null, $this->resultShouldBeTransformedToArray);

        return $mappedModel;
    }

    /**
     * @return bool
     */
    public function resultShouldBeTransformedToArray()
    {
        return $this->resultShouldBeTransformedToArray;
    }

    public function useSandbox(string $url = null)
    {
        $this->url = $this->config['sandboxUrl'];
    }

    /**
     * @param $timeOut int
     */
    public function setTimeOut(int $timeOut)
    {
        $this->timeOut = $timeOut;

        return $this;
    }

    public function isJsonContainVariadicType(): bool
    {
        return $this->jsonContainVariadicType;
    }

    public function getPathsToVariadicTypesAndValue(): array
    {
        return $this->pathsToVariadicTypesAndValue;
    }

    public function getPathsToDictionary(): array
    {
        return $this->pathsToDictionary;
    }

    public function addCustomFunctionForPath(array $customFunction)
    {
        $this->customFunctionForPaths = array_merge($this->customFunctionForPaths, $customFunction);

        return $this;
    }

    public function getCustomFunctionForPaths(): array
    {
        return $this->customFunctionForPaths;
    }

    private function initDefaultMethods()
    {
        $this->addCustomFunctionForPath($this->initCustomFunctionForPaths());

    }

    protected function initCustomFunctionForPaths(): array
    {
        return [];
    }

    public function isUseNewMapper(): bool
    {
        return $this->useNewMapper;
    }

    /**
     * @return string
     */
    public function getHttpMethod()
    {
        return $this->method;
    }

    /**
     * @return string|null
     */
    public function getRequestToFunction()
    {
        return $this->requestToFunction;
    }

    public function getPayload(): array
    {
        return $this->payload ?? [];
    }
}
