// @ts-ignore
import request from 'request';
import querystring, {stringify} from 'querystring';
import {$log} from "ts-log-debug";
import {createHash, randomBytes} from "crypto";
import detect from "detect-port";
import express from 'express';
import {ExpectedError} from "clime";
import Configstore from "configstore";
import opn from 'opn';
import {HttpClient} from "typed-rest-client/HttpClient";
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';

const PORT_OPTIONS = [8888, 7777, 6666, 1337];
const CALLBACK_ROOT = 'http://localhost:';
const CLIENT_ID = 'KOrBkAlpYgN0H5x1O37Oz491JP445UBn';
const API_ROOT = 'https://modelnest.auth0.com';
const LOGIN_ROOT = `${API_ROOT}/authorize`;
const TOKEN_ROOT = `${API_ROOT}/oauth/token`;
const SUCCESS_URL = 'https://help.modelnest.io/authentication-success/';
const ERROR_URL = 'https://i.kym-cdn.com/entries/icons/mobile/000/000/028/Fail-Stamp-Transparent_copy.jpg';
const JWKS_URL = `${API_ROOT}/.well-known/jwks.json`;
const KID = 'RTlBMDhGNUZCODA1RENGMUU1MDFGMTBBQTdEODRGNjBCQUY4NDdFQw';

export default class Auth {

    private client: HttpClient = new HttpClient('model-nest-cli');
    private port?: number;
    private verifier: string;
    private server: any;
    private challenge: string;
    private scopes: string;
    private responseType: string;
    private configStore: Configstore;
    private state: string;
    private jwksClient: any = jwksClient({
        jwksUri: JWKS_URL,
    });

    constructor() {
        this.verifier = this.base64URLEncode(randomBytes(32));
        this.state = this.base64URLEncode(randomBytes(16));
        this.challenge = this.base64URLEncode(this.sha256(this.verifier));
        this.scopes = 'openid offline_access';
        this.responseType = 'code';
        this.configStore = new Configstore('model-nest-cli');
    }


    private base64URLEncode(str: any) {
        return str.toString('base64')
            .replace(/\+/g, '-')
            .replace(/\//g, '_')
            .replace(/=/g, '');
    }

    sha256(buffer: any) {
        return createHash('sha256').update(buffer).digest();
    }


    private async getAvailablePort(): Promise<number> {
        let port;
        for(const portOption of PORT_OPTIONS) {
            const detected = await detect(portOption);
            $log.debug(`Detected port ${detected} option ${portOption}`);
            if (detected === portOption) {
                port = portOption;
                break;
            }
        }
        if (!port) {
            throw new ExpectedError('Unable to find open port for login flow');
        }
        return port;
    }

    private async getLoginUrl(): Promise<string> {
        const params = {
            response_type: this.responseType,
            code_challenge: this.challenge,
            code_challenge_method: 'S256',
            client_id: CLIENT_ID,
            redirect_uri: this.getCallbackUrl(),
            scope: this.scopes,
            audience: 'https://api.modelnest.io',
            state: this.state,
        };
        const paramUrl = querystring.stringify(params);
        const url = `${LOGIN_ROOT}?${paramUrl}`;
        return url;
    }

    private getCallbackUrl() {
        return `${CALLBACK_ROOT}${this.port}/callback`
    }

    private async verifyToken(token: string): Promise<boolean> {
        const publicKey = await this.getPublicKey();
        jwt.verify(token, publicKey, {algorithms: ['RS256']});
        return true;
    }

    private async getPublicKey(): Promise<string> {
        return new Promise<string>((resolve, reject) => {
            this.jwksClient.getSigningKey(KID, (err:any, key:any) => {
                if (err) return reject(err);
                const signingKey = key.publicKey || key.rsaPublicKey;
                resolve(signingKey);
            });
        });
    }

    private async refresh(): Promise<boolean> {
        const currentToken = this.configStore.get('auth');
        const body = {
            grant_type: 'refresh_token',
            client_id: CLIENT_ID,
            refresh_token: currentToken.refresh_token,
        };
        const headers = { 'content-type': 'application/json' };
        const resp = await this.client.post(TOKEN_ROOT, JSON.stringify(body), headers);
        const token = await resp.readBody();
        const auth = JSON.parse(token);
        if (auth.error) {
            throw new ExpectedError('Authentication failed');
        }
        auth.refresh_token = currentToken.refresh_token;
        $log.debug(JSON.parse(token));
        if (auth.access_token) {
            this.configStore.set('auth', auth);
        }
        return true;
    }

    public async getToken(): Promise<string> {
        let currentToken = this.configStore.get('auth');
        try {
            await this.verifyToken(currentToken.access_token);
            return currentToken.access_token;
        } catch (err) {
            await this.refresh();
            currentToken = this.configStore.get('auth');
            return currentToken.access_token;
        }
    }

    public handleError(message: string, res: any, reject: any) {
        $log.error(message);
        res.redirect(301, ERROR_URL);
        reject(new ExpectedError('Login failed'));
    }

    public async login(): Promise<boolean> {
        const app = express();

        this.port = await this.getAvailablePort();
        const url = await this.getLoginUrl();
        console.log(url);
        opn(url, {wait: false});

        this.server = app.listen( this.port );
        return new Promise<boolean>((resolve, reject)=> {
            app.get('/callback', async (req: any, res: any) => {
                const error = req.query.error;
                const code = req.query.code;
                const state = req.query.state;

                if (error) {
                    this.handleError(req.query.error_description, res, reject);
                }

                const body = {
                    grant_type: 'authorization_code',
                    code_verifier: this.verifier,
                    code: code,
                    client_id: CLIENT_ID,
                    redirect_uri: this.getCallbackUrl(),
                };
                const headers = { 'content-type': 'application/json' };
                try {
                    const resp = await this.client.post(TOKEN_ROOT, JSON.stringify(body), headers);
                    const authString = await resp.readBody();
                    $log.debug(JSON.parse(authString));
                    const auth = JSON.parse(authString);
                    await this.verifyToken(auth.access_token);
                    if (auth.access_token) {
                        this.configStore.set('auth', auth);
                    }

                    res.redirect(301, SUCCESS_URL);
                } catch (err) {
                    this.handleError(err.message, res, reject);
                }
                this.server.close();
            });
        });
    }
}