const Controller = require('libflitter/controller/Controller')
const is_absolute_url = require('is-absolute-url')

class OpenIDController extends Controller {
    static get services() {
        return [...super.services, 'Vue', 'openid_connect', 'configs', 'models', 'output']
    }

    fail(res, message) {
        return this.Vue.auth_message(res, { message, next_destination: '/dash' })
    }

    async get_clients(req, res) {
        const Client = this.models.get('openid:Client')
        const clients = await Client.find()

        const data = []
        for ( const client of clients ) {
            data.push(await client.to_api())
        }

        return res.api(data)
    }

    async get_client(req, res) {
        const Client = this.models.get('openid:Client')
        const client = await Client.findById(req.params.id)

        if ( !client )
            return res.status(404)
                .message(req.T('api.client_not_found'))
                .api()

        return res.api(await client.to_api())
    }

    async create_client(req, res) {
        const required_fields = ['client_name', 'grant_types', 'redirect_uri']
        for ( const field of required_fields ) {
            if ( !req.body[field] )
                return res.status(400)
                    .message(`${req.T('api.missing_field')} ${field}`)
                    .api()
        }

        if ( !Array.isArray(req.body.grant_types) )
            return res.status(400)
                .message(`${req.T('api.improper_field')} grant_types ${req.T('api.array')}`)
                .api()

        if ( !is_absolute_url(req.body.redirect_uri) )
            return res.status(400)
                .message(`${req.T('api.improper_field')} redirect_uri ${req.T('api.absolute_url')}`)
                .api()

        const payload = {
            client_name: req.body.client_name,
            grant_types: req.body.grant_types,
            redirect_uris: [req.body.redirect_uri],
        }

        const Client = this.models.get('openid:Client')
        const client = new Client({ payload })

        await client.save()
        return res.api(await client.to_api())
    }

    async update_client(req, res) {
        const Client = this.models.get('openid:Client')
        const client = await Client.findById(req.params.id)

        if ( !client )
            return res.status(404)
                .message(req.T('api.client_not_found'))
                .api()

        const required_fields = ['client_name', 'grant_types', 'redirect_uri']
        for ( const field of required_fields ) {
            if ( !req.body[field] )
                return res.status(400)
                    .message(`${req.T('api.missing_field')} ${field}`)
                    .api()
        }

        if ( !Array.isArray(req.body.grant_types) )
            return res.status(400)
                .message(`${req.T('api.improper_field')} grant_types ${req.T('api.array')}`)
                .api()

        if ( !is_absolute_url(req.body.redirect_uri) )
            return res.status(400)
                .message(`${req.T('api.improper_field')} redirect_uri ${req.T('api.absolute_url')}`)
                .api()

        client.payload.client_name = req.body.client_name
        client.payload.grant_types = req.body.grant_types
        client.payload.redirect_uris = [req.body.redirect_uri]

        await client.save()
        return res.api()
    }

    async delete_client(req, res, next) {
        const Client = this.models.get('openid:Client')
        const client = await Client.findById(req.params.id)

        if ( !client || !client.active )
            return res.status(404)
                .message(req.T('api.client_not_found'))
                .api()

        await client.delete()
        return res.api()
    }

    async handle_interaction(req, res) {
        const {
            uid, prompt, params, session,
        } = await this.openid_connect.provider.interactionDetails(req, res)

        const name = prompt.name
        if ( typeof this[name] !== 'function' ) {
            return this.fail(res, 'Sorry, something has gone wrong.')
        }

        return this[name](req, res, { uid: uid.toLowerCase(), prompt, params, session })
    }

    async consent(req, res, { uid, prompt, params, session }) {
        const Client = this.models.get('openid:Client')
        const { details: { scopes, claims } } = prompt
        const { client_id, redirect_uri } = params

        const client_raw = await Client.findById(client_id)
        const client = client_raw.to_api()
        const uri = new URL(redirect_uri)

        const Application = this.models.get('Application')
        const Policy = this.models.get('iam:Policy')
        const application = await Application.findOne({ openid_client_ids: params.client_id })
        if ( !application ) {
            this.output.warn('IAM Denial!')
            return this.Vue.auth_message(res, {
                message: req.T('saml.no_access').replace('APP_NAME', 'this application'),
                next_destination: '/dash',
            })
        } else if ( !(await Policy.check_user_access(req.user, application.id)) ) {
            this.output.warn('IAM Denial!')
            return this.Vue.auth_message(res, {
                message: req.T('saml.no_access').replace('APP_NAME', application.name),
                next_destination: '/dash',
            })
        }

        // If the user has already authorized this app, just redirect
        if ( req.user.has_authorized({ id: application.id }) ) {
            return res.redirect(`/openid/interaction/${uid.toLowerCase()}/grant`)
        }

        // Otherwise, prompt them for authorization
        return res.page('public:message', {
            ...this.Vue.data({
                message: `<h3 class="font-weight-light">Authorize ${application.name}?</h3>
                        <br>
                        ${req.T('auth.oauth_prompt').replace('CLIENT_NAME', application.name).replace('APP_NAME', this.configs.get('app.name'))}
                        <br><br><br>
                        <i><small>${req.T('auth.will_redirect')} ${uri.host}</small></i>`,

                actions: [
                    {
                        text: req.T('common.deny'),
                        action: 'redirect',
                        next: '/dash',
                    },
                    {
                        text: req.T('common.grant'),
                        action: 'redirect',
                        next: `/openid/grant-and-save/${application.id}/${uid.toLowerCase()}`,
                    },
                    {
                        text: req.T('common.grant_once'),
                        action: 'redirect',
                        next: `/openid/interaction/${uid.toLowerCase()}/grant`,
                    },
                ],
            })
        })
    }

    async grant_and_save(req, res, next) {
        if ( !req.user.has_authorized({ id: req.params.app_id }) ) {
            req.user.authorize({
                id: req.params.app_id,
                api_scopes: ['openid-connect'],
            })

            await req.user.save()
        }

        return res.redirect(`/openid/interaction/${req.params.uid.toLowerCase()}/grant`)
    }

    async login(req, res, { uid, prompt, params, session }) {
        return res.redirect(`/openid/interaction/${uid.toLowerCase()}/start-session`)
    }

    /**
     * The user has been logged in, so set the OpenID acccount ID and redirect.
     * @param req
     * @param res
     * @returns {Promise<void>}
     */
    async start_session(req, res) {
        const {
            uid, prompt, params, session,
        } = await this.openid_connect.provider.interactionDetails(req, res)

        if ( prompt.name !== 'login' ) {
            return this.fail(res,'Sorry, something has gone wrong.')
        }

        const Application = this.models.get('Application')
        const Policy = this.models.get('iam:Policy')
        const application = await Application.findOne({ openid_client_ids: params.client_id })
        if ( !application ) {
            this.output.warn('IAM Denial!')
            return this.Vue.auth_message(res, {
                message: req.T('saml.no_access').replace('APP_NAME', 'this application'),
                next_destination: '/dash',
            })
        } else if ( !(await Policy.check_user_access(req.user, application.id)) ) {
            this.output.warn('IAM Denial!')
            return this.Vue.auth_message(res, {
                message: req.T('saml.no_access').replace('APP_NAME', application.name),
                next_destination: '/dash',
            })
        }

        const result = {
            select_account: {},
            login: {
                account: req.user.id,
            },
        }

        await this.openid_connect.provider.interactionFinished(req, res, result, { mergeWithLastSubmission: true })
    }

    async process_grant(req, res) {
        const {
            uid, prompt, params, session,
        } = await this.openid_connect.provider.interactionDetails(req, res)

        if ( prompt.name !== 'consent' ) {
            return this.fail(res,'Sorry, something has gone wrong.')
        }

        const Application = this.models.get('Application')
        const Policy = this.models.get('iam:Policy')
        const application = await Application.findOne({ openid_client_ids: params.client_id })
        if ( !application ) {
            this.output.warn('IAM Denial!')
            return this.Vue.auth_message(res, {
                message: req.T('saml.no_access').replace('APP_NAME', 'this application'),
                next_destination: '/dash',
            })
        } else if ( !(await Policy.check_user_access(req.user, application.id)) ) {
            this.output.warn('IAM Denial!')
            return this.Vue.auth_message(res, {
                message: req.T('saml.no_access').replace('APP_NAME', application.name),
                next_destination: '/dash',
            })
        }

        // TODO allow listing of scopes/claims and rejecting
        const consent = {
            rejectedScopes: [],
            rejectedClaims: [],
            replace: false,
        }

        const result = { consent }
        await this.openid_connect.provider.interactionFinished(req, res, result, { mergeWithLastSubmission: true })
    }
}

module.exports = OpenIDController
