<?php
/*
 * PSX is an open source PHP framework to develop RESTful APIs.
 * For the current version and information visit <https://phpsx.org>
 *
 * Copyright 2010-2022 Christoph Kappestein <christoph.kappestein@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

namespace PSX\Framework\Filter;

use Closure;
use PSX\Http\Exception\UnauthorizedException;
use PSX\Http\FilterChainInterface;
use PSX\Http\FilterInterface;
use PSX\Http\RequestInterface;
use PSX\Http\ResponseInterface;

/**
 * Oauth2Authentication
 *
 * @author  Christoph Kappestein <christoph.kappestein@gmail.com>
 * @license http://www.apache.org/licenses/LICENSE-2.0
 * @link    http://phpsx.org
 */
class Oauth2Authentication implements FilterInterface
{
    private \Closure $accessCallback;
    private ?string $realm = null;
    private ?\Closure $successCallback = null;
    private ?\Closure $failureCallback = null;
    private ?\Closure $missingCallback = null;

    /**
     * The accessCallback is called with the provided access token. At the moment this class supports only Bearer
     * authentication. If the accessCallback explicit return true the authorization was successful
     */
    public function __construct(Closure $accessCallback, ?string $realm = null)
    {
        $this->accessCallback = $accessCallback;
        $this->realm = $realm;

        $this->onSuccess(function () {
            // authentication successful
        });

        $this->onFailure(function () {
            $params = array(
                'realm' => $this->realm ?: 'psx',
            );

            throw new UnauthorizedException('Invalid access token', 'Bearer', $params);
        });

        $this->onMissing(function (ResponseInterface $response) {
            $params = array(
                'realm' => $this->realm ?: 'psx',
            );

            throw new UnauthorizedException('Missing authorization header', 'Bearer', $params);
        });
    }

    public function handle(RequestInterface $request, ResponseInterface $response, FilterChainInterface $filterChain): void
    {
        $authorization = $request->getHeader('Authorization');

        if (!empty($authorization)) {
            $parts       = explode(' ', $authorization, 2);
            $type        = $parts[0] ?? null;
            $accessToken = $parts[1] ?? null;

            if ($type === 'Bearer' && !empty($accessToken)) {
                $result = call_user_func_array($this->accessCallback, array($accessToken));

                if ($result === true) {
                    $this->callSuccess($response);

                    $filterChain->handle($request, $response);
                } else {
                    $this->callFailure($response);
                }
            } else {
                $this->callMissing($response);
            }
        } else {
            $this->callMissing($response);
        }
    }

    public function onSuccess(Closure $successCallback)
    {
        $this->successCallback = $successCallback;
    }

    public function onFailure(Closure $failureCallback)
    {
        $this->failureCallback = $failureCallback;
    }

    public function onMissing(Closure $missingCallback)
    {
        $this->missingCallback = $missingCallback;
    }

    protected function callSuccess(ResponseInterface $response)
    {
        call_user_func_array($this->successCallback, array($response));
    }

    protected function callFailure(ResponseInterface $response)
    {
        call_user_func_array($this->failureCallback, array($response));
    }

    protected function callMissing(ResponseInterface $response)
    {
        call_user_func_array($this->missingCallback, array($response));
    }
}
