const { http, https } = require('follow-redirects');
const querystring = require('querystring');
const services = require('./services');
const crypto = require('crypto');

class OAuth2 {

    constructor(app, opts, name) {
        this.app = app;
        this.name = name;
        this.opts = opts;

        if (opts.service) {
            Object.assign(this.opts, services[opts.service]);
        }

        this.access_token = opts.access_token || app.getSession(`${name}_access_token`);
        this.refresh_token = opts.refresh_token || app.getSession(`${name}_refresh_token`);

        if (this.access_token) {
            let expires = app.getSession(`${name}_expires`);

            if (expires && expires < expires_in(-10)) {
                this.access_token = null;

                app.removeSession(`${name}_access_token`);
                app.removeSession(`${name}_expires`);

                if (this.refresh_token) {
                    this.refreshToken(this.refresh_token);
                }
            }
        }
    }

    async init() {
        if (this.opts.jwt_bearer && !this.access_token) {
            const assertion = this.app.getJSONWebToken(this.opts.jwt_bearer);
            let response = await this.grant('urn:ietf:params:oauth:grant-type:jwt-bearer', { assertion });
            this.access_token = response.access_token;
        }

        if (this.opts.client_credentials && !this.access_token) {
            let response = await this.grant('client_credentials');
            this.access_token = response.access_token;
        }
    }

    async authorize(scopes = [], params = {}) {
        let query = this.app.req.query;

        if (query.state && query.state == this.app.getSession(`${this.name}_state`)) {
            this.app.removeSession(`${this.name}_state`);

            if (query.error) {
                throw new Error(query.error_message || query.error);
            }

            if (query.code) {
                let params = {
                    redirect_uri: redirect_uri(this.app.req),
                    code: query.code
                };

                if (this.app.getSession(`${this.name}_code_verifier`)) {
                    params.code_verifier = this.app.getSession(`${this.name}_code_verifier`);
                }

                return this.grant('authorization_code', params);
            }
        }

        if (this.opts.pkce || params.code_verifier) {
            const code_verifier = params.code_verifier || crypto.randomBytes(40).toString('hex');
            this.app.setSession(`${this.name}_code_verifier`, code_verifier);
            params.code_challenge_method = 'S256';
            params.code_challenge = crypto.createHash('sha256').update(code_verifier).digest('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
        }

        params = Object.assign({}, {
            response_type: 'code',
            client_id: this.opts.client_id,
            scope: scopes.join(this.opts.scope_separator),
            redirect_uri: redirect_uri(this.app.req),
            state: generate_state()
        }, this.opts.params, params);

        this.app.setSession(`${this.name}_state`, params.state);

        this.app.res.redirect(build_url(this.opts.auth_endpoint, params));
    }

    async refreshToken(refresh_token) {
        return this.grant('refresh_token', { refresh_token });
    }

    async grant(type, params) {
        const endpoint = new URL(this.opts.token_endpoint);

        return new Promise((resolve, reject) => {
            const req = (endpoint.protocol == 'https:' ? https : http).request(endpoint, {
                method: 'POST'
            }, res => {
                let body = '';
                res.setEncoding('utf8');
                res.on('data', chunk => body += chunk);
                res.on('end', () => {
                    if (res.statusCode >= 400) {
                        return reject(new Error(`Http status code ${res.statusCode}. ${body}`));
                    }

                    try {
                        body = JSON.parse(body);
                    } catch (e) { }

                    if (!body.access_token) {
                        return reject(new Error(`Http response has no access_token. ${JSON.stringify(body)}`))
                    }

                    this.app.setSession(`${this.name}_access_token`, body.access_token);
                    this.access_token = body.access_token;

                    if (body.expires_in) {
                        this.app.setSession(`${this.name}_expires`, expires_in(body.expires_in));
                    }

                    if (body.refresh_token) {
                        this.app.setSession(`${this.name}_refresh_token`, body.refresh_token);
                        this.refresh_token = body.refresh_token;
                    }

                    resolve(body);
                });
            });

            const body = querystring.stringify(Object.assign({
                grant_type: type,
                client_id: this.opts.client_id,
                client_secret: this.opts.client_secret
            }, params));

            req.on('error', reject);
            req.setHeader('Content-Type', 'application/x-www-form-urlencoded');
            req.setHeader('Content-Length', body.length); // required by azure endpoint
            req.write(body);
            req.end();
        });
    }

}

function build_url(url, params) {
    return url + (url.indexOf('?') != -1 ? '&' : '?') + querystring.stringify(params);
}

function redirect_uri(req) {
    let hasProxy = !!req.get('x-forwarded-host');
    return `${req.protocol}://${hasProxy ? req.hostname : req.get('host')}${req.path}`;
}

function expires_in(s) {
    return ~~(Date.now() / 1000) + s;
}

function generate_state() {
    const { v4: uuidv4 } = require('uuid');
    return uuidv4();
}

module.exports = OAuth2;