<?php
namespace Misuzu\OAuth2;

use InvalidArgumentException;
use RuntimeException;
use Index\XArray;
use Index\Http\{HttpResponseBuilder,HttpRequest};
use Index\Http\Content\FormContent;
use Index\Http\Routing\Processors\Before;
use Index\Http\Routing\Routes\{ExactRoute,PatternRoute};
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon};
use Misuzu\{CsrfContext,Template};
use Misuzu\Auth\AuthInfo;
use Misuzu\Routing\{HandlerRoles,RouteHandler,RouteHandlerCommon};
use Misuzu\Users\UsersContext;

#[HandlerRoles('main')]
final class OAuth2WebRoutes implements RouteHandler, UrlSource {
    use RouteHandlerCommon, UrlSourceCommon;

    /** @var string[] */
    private const array RESPONSE_TYPES = ['code', 'code id_token'];

    public function __construct(
        private OAuth2Context $oauth2Ctx,
        private UsersContext $usersCtx,
        private UrlRegistry $urls,
        private CsrfContext $csrfCtx,
        private AuthInfo $authInfo,
    ) {}

    #[PatternRoute('GET', '/oauth2/authori[sz]e')]
    #[Before('authz:cookie', required: false)]
    #[UrlFormat('oauth2-authorise', '/oauth2/authorize')]
    public function getAuthorise(HttpResponseBuilder $response, HttpRequest $request): string {
        return Template::renderRaw('oauth2.authorise');
    }

    /**
     * @return array{
     *  error: 'auth'|'csrf'|'method'|'length'|'client'|'scope'|'format'|'required'|'authorise'|'resptype',
     *  scope?: string,
     *  reason?: string,
     * }|array{
     *  code: string,
     *  redirect: string,
     * }
     */
    #[ExactRoute('POST', '/oauth2/authorize')]
    #[Before('authz:cookie', required: false)]
    #[Before('input:urlencoded')]
    public function postAuthorise(HttpResponseBuilder $response, HttpRequest $request, FormContent $content): array {
        // TODO: RATE LIMITING

        if(!$this->csrfCtx->verifyToken($request->getHeaderLine('X-CSRF-token')))
            return ['error' => 'csrf'];

        $response->setHeader('X-CSRF-Token', $this->csrfCtx->createToken());

        if(!$this->authInfo->loggedIn)
            return ['error' => 'auth'];

        $responseTypes = explode(' ', (string)$content->getParam('rt'), 2);
        sort($responseTypes);
        $responseType = implode(' ', $responseTypes);
        if(!in_array($responseType, self::RESPONSE_TYPES))
            return ['error' => 'resptype'];

        $codeChallengeMethod = 'plain';
        if($content->hasParam('ccm')) {
            $codeChallengeMethod = $content->getParam('ccm');
            if(!in_array($codeChallengeMethod, ['plain', 'S256']))
                return ['error' => 'method'];
        }

        $codeChallenge = $content->getParam('cc');
        $codeChallengeLength = strlen($codeChallenge);
        if($codeChallengeMethod === 'S256') {
            if($codeChallengeLength !== 43)
                return ['error' => 'length'];
        } else {
            if($codeChallengeLength < 43 || $codeChallengeLength > 128)
                return ['error' => 'length'];
        }

        try {
            $appInfo = $this->oauth2Ctx->appsCtx->apps->getAppInfo(
                (string)$content->getParam('client'),
                deleted: false
            );
        } catch(RuntimeException $ex) {
            return ['error' => 'client'];
        }

        if($content->hasParam('scope')) {
            $scope = [];
            $scopeInfos = $this->oauth2Ctx->appsCtx->handleScopeString($appInfo, (string)$content->getParam('scope'));

            foreach($scopeInfos as $scopeName => $scopeInfo) {
                if(is_string($scopeInfo))
                    return ['error' => 'scope', 'scope' => $scopeName, 'reason' => $scopeInfo];

                $scope[] = $scopeInfo->string;
            }

            $scope = implode(' ', $scope);
        } else $scope = '';

        if($content->hasParam('redirect')) {
            $redirectUri = (string)$content->getParam('redirect');
            $redirectUriId = $this->oauth2Ctx->appsCtx->apps->getAppUriId($appInfo, $redirectUri);
            if($redirectUriId === null)
                return ['error' => 'format'];
        } else {
            $uriInfos = $this->oauth2Ctx->appsCtx->apps->getAppUriInfos($appInfo);
            if(count($uriInfos) !== 1)
                return ['error' => 'required'];

            $uriInfo = array_pop($uriInfos);
            $redirectUri = $uriInfo->string;
            $redirectUriId = $uriInfo->id;
        }

        try {
            $authsInfo = $this->oauth2Ctx->authorisations->createAuthorisation(
                $appInfo,
                $this->authInfo->userInfo,
                $redirectUriId,
                $codeChallenge,
                $codeChallengeMethod,
                $scope
            );
        } catch(RuntimeException $ex) {
            return ['error' => 'authorise', 'detail' => $ex->getMessage()];
        }

        $result = [
            'redirect' => $redirectUri,
            'code' => $authsInfo->code,
        ];

        if(in_array('id_token', $responseTypes))
            $result['id_token'] = $this->oauth2Ctx->createIdToken($appInfo, $authsInfo);

        return $result;
    }

    /**
     * @return array{
     *  error: 'auth'|'csrf'|'client'|'scope'|'format'|'required',
     *  scope?: string,
     *  reason?: string,
     * }|array{
     *  app: array{
     *   name: string,
     *   summary: string,
     *   trusted: bool,
     *   links: array{ title: string, display: string, uri: string }[]
     *  },
     *  user: array{
     *   name: string,
     *   colour: string,
     *   profile_uri: string,
     *   avatar_uri: string,
     *   guise?: array{
     *    name: string,
     *    colour: string,
     *    profile_uri: string,
     *    revert_uri: string,
     *    avatar_uri: string,
     *   },
     *  },
     *  scope: string[],
     * }
     */
    #[ExactRoute('GET', '/oauth2/resolve-authorise-app')]
    #[Before('authz:cookie', required: false)]
    #[UrlFormat('oauth2-resolve-authorise-app', '/oauth2/resolve-authorise-app')]
    public function getResolveAuthorise(HttpResponseBuilder $response, HttpRequest $request): array {
        // TODO: RATE LIMITING

        if(!$this->csrfCtx->verifyToken($request->getHeaderLine('X-CSRF-token')))
            return ['error' => 'csrf'];

        $response->setHeader('X-CSRF-Token', $this->csrfCtx->createToken());

        if(!$this->authInfo->loggedIn)
            return ['error' => 'auth'];

        try {
            $appInfo = $this->oauth2Ctx->appsCtx->apps->getAppInfo(
                (string)$request->getParam('client'),
                deleted: false
            );
        } catch(RuntimeException $ex) {
            return ['error' => 'client'];
        }

        if($request->hasParam('redirect')) {
            $redirectUri = (string)$request->getParam('redirect');
            if($this->oauth2Ctx->appsCtx->apps->getAppUriId($appInfo, $redirectUri) === null)
                return ['error' => 'format'];
        } else {
            $uriInfos = $this->oauth2Ctx->appsCtx->apps->getAppUriInfos($appInfo);
            if(count($uriInfos) !== 1)
                return ['error' => 'required'];
        }

        $scope = [];
        if($request->hasParam('scope')) {
            $scopeInfos = $this->oauth2Ctx->appsCtx->handleScopeString($appInfo, (string)$request->getParam('scope'));

            foreach($scopeInfos as $scopeName => $scopeInfo) {
                if(is_string($scopeInfo))
                    return ['error' => 'scope', 'scope' => $scopeName, 'reason' => $scopeInfo];

                $scope[] = $scopeInfo->summary;
            }
        }

        $result = [
            'app' => [
                'name' => $appInfo->name,
                'summary' => $appInfo->summary,
                'trusted' => $appInfo->trusted,
                'links' => [
                    ['title' => 'Website', 'display' => $appInfo->websiteForDisplay, 'uri' => $appInfo->website],
                ],
            ],
            'user' => [
                'name' => $this->authInfo->userInfo->name,
                'colour' => (string)$this->usersCtx->getUserColour($this->authInfo->userInfo),
                'profile_uri' => $this->urls->format('user-profile', ['user' => $this->authInfo->userInfo->id]),
                'avatar_uri' => $this->urls->format('user-avatar', ['user' => $this->authInfo->userInfo->id, 'res' => 120]),
            ],
            'scope' => $scope,
        ];

        if($this->authInfo->impersonating)
            $result['user']['guise'] = [
                'name' => $this->authInfo->realUserInfo->name,
                'colour' => (string)$this->usersCtx->getUserColour($this->authInfo->realUserInfo),
                'profile_uri' => $this->urls->format('user-profile', ['user' => $this->authInfo->realUserInfo->id]),
                'revert_uri' => $this->urls->format('auth-revert', ['csrf' => $this->csrfCtx->createToken()]),
                'avatar_uri' => $this->urls->format('user-avatar', ['user' => $this->authInfo->realUserInfo->id, 'res' => 60]),
            ];

        return $result;
    }

    #[ExactRoute('GET', '/oauth2/verify')]
    #[Before('authz:cookie', required: false)]
    #[UrlFormat('oauth2-verify', '/oauth2/verify')]
    public function getVerify(HttpResponseBuilder $response, HttpRequest $request): string {
        return Template::renderRaw('oauth2.verify');
    }

    /**
     * @return array{
     *  error: 'auth'|'csrf'|'invalid'|'code'|'expired'|'approval'|'code'|'scope',
     *  scope?: string,
     *  reason?: string,
     * }|array{
     *  approval: 'approved'|'denied',
     * }
     */
    #[ExactRoute('POST', '/oauth2/verify')]
    #[Before('authz:cookie', required: false)]
    #[Before('input:urlencoded')]
    public function postVerify(HttpResponseBuilder $response, HttpRequest $request, FormContent $content): array {
        // TODO: RATE LIMITING

        if(!$this->csrfCtx->verifyToken($request->getHeaderLine('X-CSRF-token')))
            return ['error' => 'csrf'];

        $response->setHeader('X-CSRF-Token', $this->csrfCtx->createToken());

        if(!$this->authInfo->loggedIn)
            return ['error' => 'auth'];

        $approve = (string)$content->getParam('approve');
        if(!in_array($approve, ['yes', 'no']))
            return ['error' => 'invalid'];

        try {
            $deviceInfo = $this->oauth2Ctx->devices->getDeviceInfo(
                userCode: (string)$content->getParam('code')
            );
        } catch(RuntimeException $ex) {
            return ['error' => 'code'];
        }

        if($deviceInfo->expired)
            return ['error' => 'expired'];

        if(!$deviceInfo->pending)
            return ['error' => 'approval'];

        $approved = $approve === 'yes';

        $error = null;
        if($approved) {
            try {
                $appInfo = $this->oauth2Ctx->appsCtx->apps->getAppInfo(
                    $deviceInfo->appId,
                    deleted: false
                );
            } catch(RuntimeException $ex) {
                return ['error' => 'code'];
            }

            $scopeInfos = $this->oauth2Ctx->appsCtx->handleScopeString($appInfo, $deviceInfo->scope);
            foreach($scopeInfos as $scopeName => $scopeInfo) {
                if(is_string($scopeInfo)) {
                    $approved = false;
                    $error = ['error' => 'scope', 'scope' => $scopeName, 'reason' => $scopeInfo];
                    break;
                }

                $scope[] = $scopeInfo->summary;
            }
        }

        $this->oauth2Ctx->devices->setDeviceApproval($deviceInfo, $approved, $this->authInfo->userInfo);

        if($error !== null)
            return $error;

        return [
            'approval' => $approved ? 'approved' : 'denied',
        ];
    }

    /**
     * @return array{
     *  error: 'auth'|'csrf'|'code'|'expired'|'approval'|'scope',
     *  scope?: string,
     *  reason?: string,
     * }|array{
     *  req: array{
     *   code: string,
     *  },
     *  app: array{
     *   name: string,
     *   summary: string,
     *   trusted: bool,
     *   links: array{ title: string, display: string, uri: string }[]
     *  },
     *  user: array{
     *   name: string,
     *   colour: string,
     *   profile_uri: string,
     *   avatar_uri: string,
     *   guise?: array{
     *    name: string,
     *    colour: string,
     *    profile_uri: string,
     *    revert_uri: string,
     *    avatar_uri: string,
     *   },
     *  },
     *  scope: string[],
     * }
     */
    #[ExactRoute('GET', '/oauth2/resolve-verify')]
    #[Before('authz:cookie', required: false)]
    #[UrlFormat('oauth2-resolve-verify', '/oauth2/resolve-verify')]
    public function getResolveVerify(HttpResponseBuilder $response, HttpRequest $request) {
        // TODO: RATE LIMITING

        if(!$this->csrfCtx->verifyToken($request->getHeaderLine('X-CSRF-token')))
            return ['error' => 'csrf'];

        $response->setHeader('X-CSRF-Token', $this->csrfCtx->createToken());

        if(!$this->authInfo->loggedIn)
            return ['error' => 'auth'];

        try {
            $deviceInfo = $this->oauth2Ctx->devices->getDeviceInfo(userCode: (string)$request->getParam('code'));
        } catch(RuntimeException $ex) {
            return ['error' => 'code'];
        }

        if($deviceInfo->expired)
            return ['error' => 'expired'];

        if(!$deviceInfo->pending)
            return ['error' => 'approval'];

        try {
            $appInfo = $this->oauth2Ctx->appsCtx->apps->getAppInfo(
                $deviceInfo->appId,
                deleted: false
            );
        } catch(RuntimeException $ex) {
            return ['error' => 'code'];
        }

        $scope = [];
        $scopeInfos = $this->oauth2Ctx->appsCtx->handleScopeString($appInfo, $deviceInfo->scope);
        foreach($scopeInfos as $scopeName => $scopeInfo) {
            if(is_string($scopeInfo))
                return ['error' => 'scope', 'scope' => $scopeName, 'reason' => $scopeInfo];

            $scope[] = $scopeInfo->summary;
        }

        $result = [
            'req' => [
                'code' => $deviceInfo->userCode,
            ],
            'app' => [
                'name' => $appInfo->name,
                'summary' => $appInfo->summary,
                'trusted' => $appInfo->trusted,
                'links' => [
                    ['title' => 'Website', 'display' => $appInfo->websiteForDisplay, 'uri' => $appInfo->website],
                ],
            ],
            'scope' => $scope,
            'user' => [
                'name' => $this->authInfo->userInfo->name,
                'colour' => (string)$this->usersCtx->getUserColour($this->authInfo->userInfo),
                'profile_uri' => $this->urls->format('user-profile', ['user' => $this->authInfo->userInfo->id]),
                'avatar_uri' => $this->urls->format('user-avatar', ['user' => $this->authInfo->userInfo->id, 'res' => 120]),
            ],
        ];

        if($this->authInfo->impersonating)
            $result['user']['guise'] = [
                'name' => $this->authInfo->realUserInfo->name,
                'colour' => (string)$this->usersCtx->getUserColour($this->authInfo->realUserInfo),
                'profile_uri' => $this->urls->format('user-profile', ['user' => $this->authInfo->realUserInfo->id]),
                'revert_uri' => $this->urls->format('auth-revert', ['csrf' => $this->csrfCtx->createToken()]),
                'avatar_uri' => $this->urls->format('user-avatar', ['user' => $this->authInfo->realUserInfo->id, 'res' => 60]),
            ];

        return $result;
    }
}