<?php

namespace app\controllers\v1;

use bizley\jwt\Jwt;
use Exception;
use Lcobucci\JWT\UnencryptedToken;
use Lcobucci\JWT\Validation\Constraint\IssuedBy;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
use Yii;
use yii\helpers\ArrayHelper;
use yii\rest\Controller;
use yii\web\Response;
use yii\web\UnauthorizedHttpException;

abstract class BaseController extends Controller {
    // public $currentService;
    // a list of endpoint ids that shouldn't have the service token authenticated
    public $publicEndpoints = [];

    public $serviceGuid = null;

    public function behaviors() {
        $behaviours = parent::behaviors();
        unset($behaviours['rateLimiter']);
        return ArrayHelper::merge(
            $behaviours,
            [
                # Custom behaviours
            ]
        );
    }

    /**
     * { @inheritdoc }
     * 
     * In this case, we perform authentication here. A list of actions to exclude from authentication is maintained in the method. If the call is to any other
     * action, we attempt to authenticate via basic auth. If this is successful, the call continues to the respective action. Other
     */
    public function beforeAction($action) {
        Yii::debug('beforeAction on ' . $action->id, __METHOD__);
        // Don't attempt to authenticate on these actions
        $exclude = $this->publicEndpoints;

        // If something failed before now, don't bother carrying on
        if (!parent::beforeAction($action)) {
            Yii::debug('Previous beforeAction failed', __METHOD__);
            return false;
        }

        // If the current action being called is in the list of excluded actions, carry on
        if (in_array($action->id, $exclude)) {
            Yii::debug('Action in exclude list', __METHOD__);
            return true;
        }

        // Extract service auth token; attempt to authenticate with it
        $request = Yii::$app->request;
        $header = ArrayHelper::getValue($request->headers, 'Service-Authorization', ArrayHelper::getValue($request->headers, 'X-Service-Authorization'));

        $allowed = $this->validateToken($header);

        // If authentication succeeds, continue to the requested action
        // Otherwise return unauthorized
        if ($allowed) {
            Yii::debug('Authenticated! Continuing...', __METHOD__);
            return true;
        } else {
            Yii::debug('Auth failed. Throwing unauthorized', __METHOD__);
            throw new UnauthorizedHttpException('Unrecognised service token');
            return false;
        }
    }

    public function validateToken($header) {
        $allowed = false;
        if ($header != null) {
            // we need to explode the jwt, validate it, then find the token
            /** @var Jwt $jwt */
            $jwt = Yii::$app->jwt;

            /** @var UnencryptedToken $token */
            $token = $jwt->parse((string) $header); // Parses from a string

            if (!ArrayHelper::getValue(Yii::$app->params, 'disableServiceAuth', false)) {
                $keys = Yii::$app->cache->get('validationKeys.service');
                if ($keys != null) {
                    foreach ($keys as $status => $key) {
                        $testKey = Yii::$app->jwt->buildKey($key);
                        Yii::$app->jwt->validationConstraints = static function (Jwt $jwt) use ($testKey) {
                            $config = $jwt->getConfiguration();
                            return ArrayHelper::merge(
                                $config->validationConstraints(),
                                [
                                    new SignedWith($config->signer(), $testKey),
                                    new IssuedBy('https://one-space.co.za')
                                ]
                            );
                        };
                        if ($jwt->validate($token)) {
                            // we just care that the token is valid. Don't really care about the contents right now
                            $allowed = true;
                            // keep track of which service this is
                            $this->serviceGuid = $token->claims()->get('sid');
                        }
                    }
                }
            } else {
                $allowed = true;
                // keep track of which service this is
                $this->serviceGuid = $token->claims()->get('sid');
            }
        }
        return $allowed;
    }


    /**
     * Allows the user to quickly return custom formatted error response with 
     * the selected response code and custom message.
     * 
     * @param   int             $code       The HTTP response code, should never be 200 in this context.
     * @param   string|array    $message    The custom message to return to the requester.
     * 
     * @return  yii\web\Response
     * 
     * @throws  Exception   If parsed code is 200
     * 
     * @access  protected
     */

    protected function customErrorResponse(int $code, string|array $message): Response {
        if ($code == 200) {
            throw new Exception("Error code should never be 200.");
        }
        Yii::$app->response->statusCode = $code;
        Yii::$app->response->format = Response::FORMAT_JSON;
        Yii::$app->response->data = [
            'success' => false,
            'code' => $code,
            'name' => Response::$httpStatuses[$code] ?? 'Unknown',
            'message' => $message,
        ];
        return Yii::$app->response;
    }
}
