<?php

namespace Snaju\Inception\Http\Request\Validators;

use Snaju\Inception\Http\Request\Validators\Rules\EmailRule;
use Snaju\Inception\Http\Request\Validators\Rules\EqualsRule;
use Snaju\Inception\Http\Request\Validators\Rules\FieldMatchesRule;
use Snaju\Inception\Http\Request\Validators\Rules\ObjectRule;
use Snaju\Inception\Http\Request\Validators\Rules\BooleanRule;
use Snaju\Inception\Http\Request\Validators\Rules\GreaterThanEqualRule;
use Snaju\Inception\Http\Request\Validators\Rules\GreaterThanRule;
use Snaju\Inception\Http\Request\Validators\Rules\InArrayRule;
use Snaju\Inception\Http\Request\Validators\Rules\IntegerRule;
use Snaju\Inception\Http\Request\Validators\Rules\JsonRule;
use Snaju\Inception\Http\Request\Validators\Rules\LessThan;
use Snaju\Inception\Http\Request\Validators\Rules\LessThanEqual;
use Snaju\Inception\Http\Request\Validators\Rules\MaxRule;
use Snaju\Inception\Http\Request\Validators\Rules\MinRule;
use Snaju\Inception\Http\Request\Validators\Rules\NumericRule;
use Snaju\Inception\Http\Request\Validators\Rules\PhoneNumberRule;
use Snaju\Inception\Http\Request\Validators\Rules\PresentRule;
use Snaju\Inception\Http\Request\Validators\Rules\RequiredRule;
use Snaju\Inception\Http\Request\Validators\Rules\StringRule;
use Snaju\Inception\Exception\InvalidArgumentException;
use Snaju\Inception\Exception\InvalidKeyException;
use Snaju\Inception\Http\Request\Validators\Rules\UniqueRule;
use Snaju\Inception\Http\Request\WebRequest;
use Snaju\Inception\InceptionCore;
use Snaju\Inception\Route\Loaders\Twig\TemplateManager;
use Snaju\Inception\Util\Loaders\Collection;


class Validator implements ValidatorInterface
{

    protected WebRequest $request;

    protected array $rules = [];

    protected array $data = [];

    protected array $errors = [];

    protected Collection $defaultRules;

    protected bool $usesRequestData = true;

//    private array $allowedParentObjects = ['headers', 'cookies', 'query', 'body'];

    public function __construct(array $rules = [], array $data = [], bool $sendErrorResponses = true)
    {
        $this->request = InceptionCore::getRequest();
        $this->defaultRules = new Collection([
            'array' => ObjectRule::class,
            'bool' => BooleanRule::class,
            'gte' => GreaterThanEqualRule::class,
            'gt' => GreaterThanRule::class,
            'in_array' => InArrayRule::class,
            'integer' => IntegerRule::class,
            'json' => JsonRule::class,
            'lt' => LessThan::class,
            'lte' => LessThanEqual::class,
            'max' => MaxRule::class,
            'min' => MinRule::class,
            'numeric' => NumericRule::class,
//            'object'        => ObjectRule::class,
            'present' => PresentRule::class, // should be first in validator string, bails if fail
            'required' => RequiredRule::class, // should be first in validator string, bails if fail
            'string' => StringRule::class,
            'unique' => UniqueRule::class,
            'phone' => PhoneNumberRule::class,
            'matches' => FieldMatchesRule::class,
            'email' => EmailRule::class,
            'eq' => EqualsRule::class
        ]);

        $this->rules = $rules;

        // Use the request data if no data was specified
        if (count($data) > 0) {
            $this->data = $data;
            $this->usesRequestData = false;
        } else {
            $this->data = $this->request->__dangerouslytoArray();
            $this->usesRequestData = true;
        }

        if (count($rules) > 0) {
            $this->validate($this->data, $sendErrorResponses);
        }
    }

    public function appendRule($token, $ruleString): Validator
    {
        $this->rules[$token] = $ruleString;
        return $this;
    }

    public function revalidate(bool $sendErrorResponses = true): Validator
    {
        $this->errors = [];
        if (count($this->rules) > 0) {
            $this->validate($this->data, $sendErrorResponses);
        }
        return $this;
    }

    public function validate(array $data = [], bool $sendErrorResponses = false)
    {
        $ruleArray = array_merge([], $this->rules);

        if (count($data) == 0) {
            $data = $this->data;
        }

        foreach ($ruleArray as $key => $value) {
            if (str_contains($key, '.')) {
                return;
            } else {
                $rules = $this->createRulesObjectFromString($value);

                $optional = property_exists($rules, 'if_present');
                $nullable = property_exists($rules, 'nullable');

                // If nullable
                // If property data exist in array and not null, run validator
                // Allow empty or null value
                if ($nullable) {
                    if (!array_key_exists($key, $data['body'])) {
                        continue;
                    }
                    if (array_key_exists($key, $data['body'])) {
                        if ($data['body'][$key] === null) {
                            continue;
                        }
                    }
                }


                foreach ($rules as $ruleId => $ruleValue) {
                    if ($ruleValue instanceof RuleMetaData) {
                        if ($this->defaultRules->keyExists($ruleId)) {
                            // Retrieve the static class of the validator
                            $namespace = $this->defaultRules->get($ruleId);
                            $validator = new $namespace($key, $ruleValue->getValue(), $this->usesRequestData, $optional, $ruleValue->getErrorOverride());
                            if ($validator instanceof ValidatorRule) {
                                $keyIsValid = $validator->isValid($data);

                                if (!$keyIsValid) {
                                    $this->addError($key, $validator->getOverrideError() == null ? $validator->getErrorMessage() : $validator->getOverrideError());

                                    if ($validator->stopOnError()) {
                                        break;
                                    }
                                }
                            }
                        } else if (!in_array($ruleId, ['if_present', 'nullable'])) {
                            // TODO: throw an exception as say no validator with that key exists
                            throw new InvalidKeyException("No validator with the the key `$ruleId` exists.");
                        }
                    }
                }
            }
        }

        if ($this->hasErrors() && $sendErrorResponses) {
            $this->sendErrorResponse();
        }
    }

    public function hasErrors(): bool
    {
        $errors = false;

        if (count($this->errors) > 0) {
            $errors = true;
        }

        return $errors;
    }

    public function getErrors(): array
    {
        return $this->errors;
    }

    public function addRule(string $identifier, $rule)
    {
        if ($rule instanceof RuleInterface) {
            $this->defaultRules->add($identifier, $rule);
        } else {
            throw new InvalidArgumentException('The validation Rule must implement the RuleInterface');
        }
    }

    protected function addError(string $key, string $error)
    {
        $this->errors[$key][] = $error;
    }

    private function sendErrorResponse()
    {
        // TODO: in the future take into consideration the Accept to check what type of response to send instead of just JSON responses
        return response()->json(['errors' => $this->getErrors()], 400);
    }

    public function sendToTemplate()
    {
        // build an array for the template to have rules.
        $arr = [];
        foreach ($this->errors as $k => $err) {
            $arr[$k] = $err[0];
        }

        TemplateManager::addData("errors", $arr);
    }

    public function setCustomError($token, $msg)
    {
        $this->errors[$token][] = $msg;
    }

    public function error($token, $msg)
    {
        $this->setCustomError($token, $msg);
        $this->sendToTemplate();
    }

    public function sendToSession()
    {
        // build an array for the template to have rules.
        $arr = [];
        foreach ($this->errors as $k => $err) {
            $arr[$k] = $err[0];
        }

        $_SESSION['validator'] = $arr;
    }

    public function getErrorString(): string
    {
        $s = "";
        foreach ($this->errors as $k => $error) {
            $s .= "{$error[0]} ";
        }

        return $s;
    }

    /**
     *
     *
     * @param array $arr
     * @return array
     * @todo
     *
     */
    private function expandDotSyntaxArrayKeys(array $arr): array
    {
        $target = [];

        foreach ($arr as $key => $value) {
            if (is_array($value)) {
                $value = $this->expandDotSyntaxArrayKeys($value);
            }

//            $value = $this->createRulesObjectFromString($value);

            foreach (array_reverse(explode('.', $key)) as $key) {
                $value = [$key => $value];
            }

            $target = array_merge_recursive($target, $value);
        }

        return $target;
    }

    private function createRulesObjectFromString(string $rulesString): object
    {
        $orError = null;
        $target = [];
        $errorOverride = explode("§", $rulesString);
        if (count($errorOverride) > 1) {
            // Has a override.
            $orError = $errorOverride[1];
            $rulesString = $errorOverride[0];
        }
        $stringRules = explode('|', $rulesString);

        foreach ($stringRules as $stringRule) {
            $separator = explode(':', $stringRule);
            $rule = $separator[0];
            $value = isset($separator[1]) ? $separator[1] : 'true';

            // If the value contains comma separated values, make an array
//            if (str_contains($value, ',')) {
//                $value = explode(',', $value);
//            }

            $target[$rule] = new RuleMetaData($rule, $value, $orError);
        }

        return (object)$target;
    }

    public static function handleSession()
    {
        if (isset($_SESSION['validator'])) {
            $c = $_SESSION['validator'];
            $_SESSION['validator'] = null;
            unset($_SESSION['validator']);

            TemplateManager::addData("errors", $c);
        }
    }

    public function preserveInputs(bool $useSession = false)
    {
        foreach ($this->data['body'] as $k => $v) {
            if (!isset($this->errors[$k])) {
                TemplateManager::addData("inputs.{$k}", $v);
                // Did not have an error.
                if ($useSession) {
                    $_SESSION['inputs'][$k] = $v;
                }
            }
        }
    }

    public function toString($seperator = "<br/>")
    {
        $s = "";
        foreach ($this->errors as $f => $e) {
            $s .= "{$f}: {$e}$seperator";
        }
        return $s;
    }

}