<?php
namespace App\Middleware;

use App\Acl\Roles;
use App\Entity\IdentityInterface;
use App\Entity\Support;
use App\Entity\User;
use App\Traits\IsXmlHttpRequest;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Authentication\AuthenticationServiceInterface;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Expressive\Router\RouteResult;
use Zend\Diactoros\Response\RedirectResponse;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Expressive\Router\RouterInterface;
use Zend\Expressive\Template\TemplateRendererInterface;
use Zend\Permissions\Acl\AclInterface;
use Zend\View\Model\ViewModel;

/**
 * Class Authorization
 * @package App\Middleware
 */
class Authorization
{

    use IsXmlHttpRequest;

    /**
     * @var AuthenticationServiceInterface
     */
    protected $authentication;

    /**
     * @var AclInterface
     */
    protected $acl;
    /**
     * @var TemplateRendererInterface
     */
    protected $renderer;
    /**
     * @var RouterInterface
     */
    protected $router;
    /**
     * @var ViewModel
     */
    protected $layout;

    /**
     * Constructor.
     *
     * @param AuthenticationServiceInterface $authentication
     * @param AclInterface $acl
     * @param RouterInterface $router
     * @param TemplateRendererInterface $renderer
     * @param ViewModel $layout
     */
    public function __construct(
        AuthenticationServiceInterface $authentication,
        AclInterface $acl,
        RouterInterface $router,
        TemplateRendererInterface $renderer,
        ViewModel $layout)
    {
        $this->authentication = $authentication;
        $this->acl = $acl;

        $this->router = $router;
        $this->renderer = $renderer;
        $this->layout = $layout;
    }

    /**
     * @param ServerRequestInterface $request
     * @param ResponseInterface $response
     * @param callable|null $next
     *
     * @return JsonResponse|RedirectResponse
     */
    public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
    {
        /** @var RouteResult $route */
        $route = $request->getAttribute(RouteResult::class, false);

        if (!empty($route)) {
            $resource = $route->getMatchedRouteName();
            if ($this->authentication->hasIdentity()) {
                /** @var IdentityInterface $identity */
                $identity = $this->authentication->getIdentity();
                if ($route && !$this->acl->isAllowed($identity, $resource)) {
                    // not allowed for authorized by role
                    $result = $this->getResponseForbidden($request, $identity, $identity->getRoleId(), $resource);
                } else {
                    // allowed for authorized by role
                    $result = $next($request, $response);
                }
            } else {
                if ($route && $this->acl->isAllowed(Roles::ROLE_GUEST, $resource)) {
                    // allowed for unauthorized
                    $result = $next($request, $response);
                } else {
                    // not allowed for unauthorized
                    $result = $this->getResponseUnauthorized($request, $resource);
                }
            }
        } else {
            // @todo move to error handler
            $result = $this->getResponseNotFound($request);
        }

        return $result;
    }

    /**
     * Returns http-response "403 Forbidden"
     *
     * @param ServerRequestInterface $request
     * @param IdentityInterface $identity
     * @param $role
     * @param $resource
     * @return JsonResponse
     */
    protected function getResponseForbidden(ServerRequestInterface $request, IdentityInterface $identity, $role, $resource)
    {
        $xRequestedWith = $request->getHeader('X-Requested-With');
        $reason = null;
        $redirect = null;

        switch (true) {
            case ($this->acl->isAllowed((new Support())->setRoleId(Support::ROLE_ADMIN), $resource)) :
                $redirect = $this->router->generateUri('adm.index', ['lang' => $request->getAttribute('lang')]);
                $this->layout->setTemplate('layout/support');
                break;
            /** @noinspection PhpMissingBreakStatementInspection */
            case ($this->acl->isAllowed((new User())->setActivated(User::ACTIVATED)->setBlocked(User::NOT_BLOCKED), $resource)) :
                $redirect = $this->router->generateUri('home', ['lang' => $request->getAttribute('lang')]);
            default :
                $reason = _t('Because you are blocked or not activated. Please check your email');
                $reason .= '<br/>' . _t('Or') .' <a href="' .
                    $this->router->generateUri('user.mail.send.activation', ['lang' => $request->getAttribute('layoutInfo')->getLang()]) .
                    '">' . _t('click to re-send activation mail') . '</a>';
                $this->layout->setTemplate('layout/default');
                break;
        }

        if ($this->isXmlHttpRequest($request)) {

            $response = new JsonResponse([
                'result' => false,
                'reason' => $reason,
                'redirect' => $redirect,
                'code' => 403,
                'message' => _t('Forbidden'),
                'detail' => [
                    'email' => $identity->getEmail(),
                    'role' => $role,
                    'resource' => $resource,
                ],
            ], 403);
        } else {
            $data = [
                'lang' => $request->getAttribute('layoutInfo')->getLang()
            ];
            $data['reason'] = $reason;

            $response = new HtmlResponse($this->renderer->render('error::forbidden', $data));
        }
        return $response;
    }

    /**
     * Returns http-response "401 Unauthorized"
     *
     * @param ServerRequestInterface $request
     * @param $resource
     * @return JsonResponse
     */
    protected function getResponseUnauthorized(ServerRequestInterface $request, $resource)
    {

        if ($this->isXmlHttpRequest($request)) {
            $response =  new JsonResponse([
                'result' => false,
                'redirect' => $this->acl->isAllowed((new User())->setActivated(User::ACTIVATED), $resource) ?
                    $this->router->generateUri('home', ['lang' => $request->getAttribute('lang')]) :
                    $this->router->generateUri('adm.index', ['lang' => $request->getAttribute('lang')]) ,
                'code' => 401,
                'message' => _t('Unauthorized'),
                'detail' => [
                    'resource' => $resource,
                ],
            ], 401);
        } else {
            if ($this->acl->isAllowed((new User())->setActivated(User::ACTIVATED), $resource)) {
                $this->layout->setTemplate('layout/default');
            } elseif ($this->acl->isAllowed((new Support())->setRoleId(Support::ROLE_ADMIN), $resource)) {
                $this->layout->setTemplate('layout/support');
            } else {
                $this->layout->setTemplate('layout/default');
            }

            //$body = $this->renderer->render('error::unauthorized', ['lang' => $request->getAttribute('layoutInfo')->getLang()]);
            $body = $this->renderer->render('app::index', ['lang' => $request->getAttribute('layoutInfo')->getLang()]);

            $response = new HtmlResponse($body);
        }

        return $response;
    }

    /**
     * Returns http-response "404 Not Found"
     *
     * @param ServerRequestInterface $request
     *
     * @return JsonResponse
     */
    protected function getResponseNotFound(ServerRequestInterface $request)
    {

        if ($this->isXmlHttpRequest($request)) {
            $response = new JsonResponse([
                'code' => 404,
                'message' => _t('Not Found'),
                'detail' => [
                    'target' => $request->getRequestTarget(),
                    'method' => $request->getMethod(),
                ],
            ], 404);
        } else {
            $response = new HtmlResponse($this->renderer->render('error::404', ['lang' => $request->getAttribute('layoutInfo')->getLang()]));
        }
        return $response;
    }
}