#include app/info.jsx
#include app/scope.jsx
#include header/header.js
#include header/user.jsx

const MszOAuth2AuthoriseErrors = Object.freeze({
    'invalid_request': {
        description: 'The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.',
    },
    'unauthorized_client': {
        description: 'The client is not authorised to request an authorisation code using this method.',
    },
    'access_denied': {
        description: 'The resource owner or authorization server denied the request.',
    },
    'unsupported_response_type': {
        description: 'The authorisation server does not support obtaining an authorisation code using this method.',
    },
    'invalid_scope': {
        description: 'The requested scope is invalid, unknown, or malformed.',
    },
    'server_error': {
        description: 'The authorisation server encountered an unexpected condition that prevented it from fulfilling the request.',
    },
    'temporarily_unavailable': {
        description: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.',
    },
});

const MszOAuth2Authorise = async () => {
    const queryParams = new URLSearchParams(window.location.search);
    const loading = new MszLoading({ element: '.js-loading', size: 2 });
    const header = new MszOAuth2Header;

    const fAuths = document.querySelector('.js-authorise-form');
    const eAuthsInfo = document.querySelector('.js-authorise-form-info');
    const eAuthsScope = document.querySelector('.js-authorise-form-scope');

    const dError = document.querySelector('.js-authorise-error');
    const dErrorText = dError?.querySelector('.js-authorise-error-text');

    let scope;
    let state;
    let redirectUri;
    let redirectUriRaw;

    const displayError = (error, description, documentation) => {
        if(redirectUri === undefined) {
            if(error in MszOAuth2AuthoriseErrors) {
                const errInfo = MszOAuth2AuthoriseErrors[error];
                description ??= errInfo.description;
            } else
                description = `An unknown error occurred: ${error}`;

            dErrorText.textContent = description;

            header.setSimpleData('error', 'An error occurred!');
            header.removeElement();

            loading.visible = false;
            fAuths.classList.add('hidden');
            dError.classList.remove('hidden');
            return;
        }

        const errorUri = new URL(redirectUri);
        errorUri.searchParams.set('error', error?.toString() ?? 'invalid_request');
        if(description)
            errorUri.searchParams.set('error_description', description.toString());
        if(documentation)
            errorUri.searchParams.set('error_uri', documentation.toString());
        if(state !== undefined)
            errorUri.searchParams.set('state', state.toString());

        window.location.assign(errorUri);
    };
    const translateError = (serverError, detail) => {
        if(serverError === 'auth')
            return displayError('access_denied');
        if(serverError === 'csrf')
            return displayError('invalid_request', 'Request verification failed.');
        if(serverError === 'client')
            return displayError('invalid_request', 'There is no application associated with the specified Client ID.');
        if(serverError === 'format')
            return displayError('invalid_request', 'Redirect URI specified is not registered with this application.');
        if(serverError === 'method')
            return displayError('invalid_request', 'Requested code challenge method is not supported.');
        if(serverError === 'length')
            return displayError('invalid_request', 'Code challenge length is not acceptable.');
        if(serverError === 'required')
            return displayError('invalid_request', 'A registered redirect URI must be specified.');
        if(serverError === 'scope')
            return displayError('invalid_scope', detail === undefined ? undefined : `Requested scope "${detail.scope}" is ${detail.reason}.`);
        if(serverError === 'authorise')
            return displayError('server_error', 'Server was unable to complete authorisation.');

        return displayError('invalid_request', `An unknown error occurred: ${serverError}.`);
    };

    if(queryParams.has('redirect_uri'))
        try {
            const qRedirectUriRaw = queryParams.get('redirect_uri');
            const qRedirectUri = new URL(qRedirectUriRaw);
            if(qRedirectUri.protocol !== 'https:')
                throw 'protocol must be https';

            redirectUri = qRedirectUri;
            redirectUriRaw = qRedirectUriRaw;
        } catch(ex) {
            return displayError('invalid_request', 'Invalid redirect URI specified.');
        }

    if(queryParams.has('state')) {
        const qState = queryParams.get('state');

        if(qState.length > 1000)
            return displayError('invalid_request', 'State parameter may not be longer than 255 characters.');

        state = qState;
    }

    if(queryParams.get('response_type') !== 'code')
        return displayError('unsupported_response_type');

    let codeChallengeMethod = 'plain';
    if(queryParams.has('code_challenge_method')) {
        codeChallengeMethod = queryParams.get('code_challenge_method');
        if(!['plain', 'S256'].includes(codeChallengeMethod))
            return translateError('method');
    }

    if(!queryParams.has('code_challenge'))
        return displayError('invalid_request', 'code_challenge must be specified.');

    const codeChallenge = queryParams.get('code_challenge');
    if(codeChallengeMethod === 'S256') {
        if(codeChallenge.length !== 43)
            return displayError('invalid_request', 'Specified code challenge is not a valid SHA-256 hash.');
    } else {
        if(codeChallenge.length < 43)
            return displayError('invalid_request', 'Code challenge must be at least 43 characters long.');
        if(codeChallenge.length > 128)
            return displayError('invalid_request', 'Code challenge may not be longer than 128 characters.');
    }

    if(!queryParams.has('client_id'))
        return displayError('invalid_request', 'client_id must be specified.');

    const resolveParams = new URLSearchParams;
    resolveParams.set('client', queryParams.get('client_id'));
    if(redirectUriRaw !== undefined)
        resolveParams.set('redirect', redirectUriRaw);
    if(queryParams.has('scope')) {
        scope = queryParams.get('scope');
        resolveParams.set('scope', scope);
    }

    try {
        const { body } = await $xhr.get(`/oauth2/resolve-authorise-app?${resolveParams}`, { authed: true, csrf: true, type: 'json' });
        if(!body)
            throw 'authorisation resolve failed';
        if(typeof body.error === 'string') {
            if(body.error === 'auth') {
                window.location.assign(`/auth/login.php?oauth2=1&redirect=${encodeURIComponent(`${window.location.pathname}${window.location.search}`)}`);
                return;
            }

            return translateError(body.error, body);
        }

        const userHeader = new MszOAuth2UserHeader(body.user);
        header.setElement(userHeader);

        const verifyAuthsRequest = async () => {
            const params = {
                client: queryParams.get('client_id'),
                cc: codeChallenge,
                ccm: codeChallengeMethod,
            };
            if(redirectUriRaw !== undefined)
                params.redirect = redirectUriRaw;
            if(scope !== undefined)
                params.scope = scope;

            try {
                const { body } = await $xhr.post('/oauth2/authorise', { authed: true, csrf: true, type: 'json' }, params);
                if(!body)
                    throw 'authorisation failed';
                if(typeof body.error === 'string')
                    return translateError(body.error, body);

                const authoriseUri = new URL(body.redirect);
                authoriseUri.searchParams.set('code', body.code);
                if(state !== undefined)
                    authoriseUri.searchParams.set('state', state.toString());

                window.location.assign(authoriseUri);
            } catch(ex) {
                console.error(ex);
                translateError('authorise');
            }
        };

        if(body.app.trusted && body.user.guise === undefined) {
            if(userHeader)
                userHeader.guiseVisible = false;

            verifyAuthsRequest();
            return;
        }

        eAuthsInfo.replaceWith(new MszOAuth2AppInfo(body.app).element);
        eAuthsScope.replaceWith(new MszOAuth2AppScopeList(body.scope).element);

        fAuths.onsubmit = ev => {
            ev.preventDefault();

            loading.visible = true;
            fAuths.classList.add('hidden');

            if(userHeader)
                userHeader.guiseVisible = false;

            if(ev.submitter?.value === 'yes')
                verifyAuthsRequest();
            else
                displayError('access_denied');
        };

        loading.visible = false;
        fAuths.classList.remove('hidden');
    } catch(ex) {
        console.error(ex);
        displayError('server_error', 'Server was unable to respond to the client info request.');
    }
};