<?php

namespace FluentForm\Framework\Http;

use Closure;
use Exception;
use WP_REST_Request;
use WP_REST_Response;
use InvalidArgumentException;
use FluentForm\Framework\Validator\ValidationException;
use FluentForm\Framework\Database\Orm\ModelNotFoundException;

class Route
{
    protected $app = null;

    protected $restNamespace = null;

    protected $uri = null;
    
    protected $compiled = null;
    
    protected $name = '';

    protected $meta = [];

    protected $handler = null;

    protected $method = null;
    
    protected $options = [];

    protected $wheres = [];

    protected $policyHandler = null;

    protected $predefinedNamedRegx = [
        'int' => '[0-9]+',
        'alpha' => '[a-zA-Z]+',
        'alpha_num' => '[a-zA-Z0-9]+',
        'alpha_num_dash' => '[a-zA-Z0-9-_]+'
    ];


    public function __construct($app, $restNamespace, $uri, $handler, $method, $name = '')
    {
        $this->app = $app;
        $this->restNamespace = $restNamespace;
        $this->uri = $uri;
        $this->handler = $handler;
        $this->method = $method;
        $this->name = $name;
    }

    public static function create($app, $namespace, $uri, $handler, $method, $name)
    {
        return new static($app, $namespace, $uri, $handler, $method, $name);
    }

    public function name($name)
    {
        $this->name .= $name;

        return $this;
    }

    public function meta($meta)
    {
        $this->meta = $meta;

        return $this;
    }

    public function getMeta($key = '')
    {
        if ($key) {
            if (isset($this->meta[$key])) {
                return $this->meta[$key];
            }

            return;
        }
        
        return $this->meta;
    }

    public function where($identifier, $value = null)
    {
        if (!is_null($value)) {
            $this->wheres[$identifier] = $this->getValue($value);
        } else {
            foreach ($identifier as $key => $value) {
                $this->wheres[$key] = $this->getValue($value);
            }
        }

        return $this;
    }

    public function int($identifiers)
    {
        $identifiers = is_array($identifiers) ? $identifiers : func_get_args();

        foreach ($identifiers as $identifier) {
            $this->wheres[$identifier] = '[0-9]+';
        }

        return $this;
    }

    public function alpha($identifiers)
    {
        $identifiers = is_array($identifiers) ? $identifiers : func_get_args();

        foreach ($identifiers as $identifier) {
            $this->wheres[$identifier] = '[a-zA-Z]+';
        }

        return $this;
    }

    public function alphaNum($identifiers)
    {
        $identifiers = is_array($identifiers) ? $identifiers : func_get_args();

        foreach ($identifiers as $identifier) {
            $this->wheres[$identifier] = '[a-zA-Z0-9]+';
        }

        return $this;
    }

    public function alphaNumDash($identifiers)
    {
        $identifiers = is_array($identifiers) ? $identifiers : func_get_args();

        foreach ($identifiers as $identifier) {
            $this->wheres[$identifier] = '[a-zA-Z0-9-_]+';
        }

        return $this;
    }

    public function withPolicy($handler)
    {
        $this->policyHandler = $handler;
    }

    public function register()
    {
        $this->setOptions();

        $uri = $this->compileRoute($this->uri);

        return register_rest_route($this->restNamespace, "/{$uri}", $this->options);
    }

    protected function setOptions()
    {
        $this->options = [
            'args' => [
                '__meta__' => $this->meta
            ],
            'methods' => $this->method,
            'callback' => [$this, 'callback'],
            'permission_callback' => [$this, 'permissionCallback']
        ];
    }

    protected function getValue($value)
    {
        if (array_key_exists($value, $this->predefinedNamedRegx)) {
            return $this->predefinedNamedRegx[$value];
        }

        return $value;
    }

    protected function getPolicyHandler($policyHandler)
    {
        if ($policyHandler instanceof Closure) {
            return function() use ($policyHandler) {
                $policyHandler($this->app->request);
            };
        }

        if (strpos($policyHandler, '@') !== false) return $policyHandler;

        if (strpos($policyHandler, '::') !== false) return $policyHandler;
        
        if (!function_exists($policyHandler)) {
            if (is_string($this->handler) && strpos($this->handler, '@') !== false) {
                list($_, $method) = explode('@', $this->handler);
                $policyHandler = $policyHandler . '@' . $method;
            } else if (is_array($this->handler)) {
                $policyHandler = $policyHandler . '@' . $this->handler[1];
            }
        }

        return $policyHandler;
    }

    protected function compileRoute($uri)
    {
        $params = [];

        $compiledUri = preg_replace_callback('#/{(.*?)}#', function($match) use (&$params, $uri) {
            // Default regx
            $regx = '[^\s(?!/)]+';
            
            $param = trim($match[1]);

            if ($isOptional = strpos($param, '?')) {
                $param = trim($param, '?');
            }

            if (in_array($param, $params)) {
                throw new InvalidArgumentException(
                    "Duplicate parameter name '{$param}' found in {$uri}.", 500
                );
            }
            
            $params[] = $param;

            if (isset($this->wheres[$param])) {
                $regx = $this->wheres[$param];
            }

            $pattern = "/(?P<" . $param . ">" . $regx . ")";

            if ($isOptional) {
                $pattern = "(?:" . $pattern . ")?";
            }
            
            $this->options['args'][$param]['required'] = !$isOptional;
            
            return $pattern;

        }, $uri);

        return $this->compiled = $compiledUri;
    }

    public function callback(WP_REST_Request $request)
    {
        try {
            $this->setRestRequest($request);

            $response = $this->app->call(
                $this->app->parseRestHandler($this->handler),
                array_values($request->get_url_params())
            );

            if (!($response instanceof WP_REST_Response)) {
                if (is_wp_error($response)) {
                    $response = $this->sendWPError($response);
                } else {
                    $response = $this->app->response->sendSuccess($response);
                }
            }

            return $response;

        } catch (ValidationException $e) {
            return $this->app->response->sendError(
                $e->errors(), $e->getCode()
            );
        }  catch (ModelNotFoundException $e) {
            return $this->app->response->sendError([
                'message' => $e->getMessage()
            ], 404);
        } catch (Exception $e) {
            return $this->app->response->sendError([
                'message' => $e->getMessage()
            ], $e->getCode());
        }
    }

    public function permissionCallback(WP_REST_Request $request)
    {
        $this->setRestRequest($request);

        if (!$this->policyHandler) {
            return true;
        }

        $policyHandler = $this->app->parsePolicyHandler(
            $this->getPolicyHandler($this->policyHandler)
        );

        return $this->app->call($policyHandler, $request->get_url_params());
    }

    protected function setRestRequest($request)
    {
        if (!$this->app->bound('wprestrequest')) {
            unset($this->options['args']['__meta__']);
            $this->app->instance('wprestrequest', $request);
            $this->app->instance('route', $this);
        }
    }

    protected function sendWPError($response)
    {
        $code = $response->get_error_code();

        return $this->app->response->sendError(
            $response->get_error_messages(),
            is_numeric($code) ? $code : null
        );
    }
}
