import * as jwt              from 'jsonwebtoken';
import User                  from '../sharedByClients/models/User.js';
import LocalConfigKeys       from '../types/LocalConfigKeys.js';
import axios                 from 'axios';
import HttpManager           from './HttpManager.js';
import ora                   from 'ora';
import Permissions           from '../types/Permissions.js';
import ApiRoute              from '../sharedByClients/types/Dojo/ApiRoute.js';
import DojoBackendManager    from './DojoBackendManager.js';
import Config                from '../config/Config.js';
import ClientsSharedConfig   from '../sharedByClients/config/ClientsSharedConfig.js';
import DojoGitlabCredentials from '../sharedByClients/types/Dojo/DojoGitlabCredentials.js';
import * as http             from 'http';
import EventEmitter          from 'events';
import SharedConfig          from '../shared/config/SharedConfig.js';
import chalk                 from 'chalk';
import inquirer              from 'inquirer';
import GitlabToken           from '../shared/types/Gitlab/GitlabToken.js';
import open                  from 'open';
import { sessionConfigFile } from '../config/ConfigFiles.js';
import TextStyle             from '../types/TextStyle.js';
import DojoBackendHelper     from '../sharedByClients/helpers/Dojo/DojoBackendHelper.js';
import GitlabManager         from './GitlabManager.js';
import { StatusCodes }       from 'http-status-codes';


class LoginServer {
    readonly events: EventEmitter = new EventEmitter();
    private readonly server: http.Server;

    constructor() {
        this.server = http.createServer((req, res) => {
            const sendError = (error: string) => {
                this.events.emit('error', error);
                res.writeHead(StatusCodes.INTERNAL_SERVER_ERROR, { 'Content-Type': 'text/html' });
                res.write(`<html lang="en"><body><h1 style="color: red">DojoCLI login error</h1><h3>Please look at your CLI for more informations.</h3></body></html>`);
                res.end();
            };

            if ( req.url?.match(Config.login.server.route) ) {
                const urlParts = req.url.split('=');
                if ( urlParts.length > 0 ) {
                    res.writeHead(StatusCodes.OK, { 'Content-Type': 'text/html' });
                    res.write(`<html lang="en"><body><h1 style="color: green">DojoCLI login successful</h1><h3>You can close this window.</h3></body></html>`);
                    res.end();

                    this.events.emit('code', urlParts[1]);
                    return;
                }

                sendError(`Incorrect call => ${ req.url }`);
            }
        });
    }

    start() {
        try {
            this.server.listen(Config.login.server.port);
            this.events.emit('started');
        } catch ( error ) {
            this.events.emit('error', error);
        }
    }

    stop() {
        try {
            this.server.close();
            this.server.closeAllConnections();
            this.events.emit('stopped');
        } catch ( error ) {
            this.events.emit('error', error);
        }
    }
}


class SessionManager {
    public profile: User | undefined = undefined;

    get isLogged(): boolean {
        return this.apiToken !== null && this.apiToken !== '';
    }

    get apiToken(): string {
        const apisToken = sessionConfigFile.getParam(LocalConfigKeys.APIS_TOKEN) as null | { [key: string]: string };

        if ( apisToken !== null && ClientsSharedConfig.apiURL in apisToken ) {
            return apisToken[ClientsSharedConfig.apiURL];
        }

        return '';
    }

    set apiToken(token: string) {
        let apisToken = sessionConfigFile.getParam(LocalConfigKeys.APIS_TOKEN) as null | { [key: string]: string };
        if ( apisToken === null ) {
            apisToken = {};
        }
        apisToken[ClientsSharedConfig.apiURL] = token;
        sessionConfigFile.setParam(LocalConfigKeys.APIS_TOKEN, apisToken);

        try {
            const payload = jwt.decode(token);

            if ( payload && typeof payload === 'object' && payload.profile ) {
                this.profile = payload.profile as User;
            }
        } catch ( error ) {
            this.profile = undefined;
        }
    }

    get gitlabCredentials(): DojoGitlabCredentials {
        return sessionConfigFile.getParam(LocalConfigKeys.GITLAB) as DojoGitlabCredentials;
    }

    set gitlabCredentials(credentials: DojoGitlabCredentials) {
        sessionConfigFile.setParam(LocalConfigKeys.GITLAB, credentials);

        if ( credentials.accessToken ) {
            GitlabManager.setToken(credentials.accessToken);
        }
    }

    private async getGitlabCodeFromHeadlessEnvironment(): Promise<string> {
        const indent: string = '    ';
        console.log(`${ indent }Please open the following URL in your web browser and accept to give the requested permissions to Dojo:`);
        console.log(TextStyle.URL(`${ indent }${ Config.login.gitlab.url.code }`));
        console.log(`${ indent }Then, copy the code at the end of the redirected url and paste it bellow.`);
        console.log(`${ indent }Example of url (here the code is 123456): ${ TextStyle.URL(SharedConfig.login.gitlab.url.redirect + '?code=') }${ chalk.green('123456') }`);
        return (await inquirer.prompt({
                                          type   : 'password',
                                          name   : 'code',
                                          message: `${ chalk.green('?') } Please paste the Gitlab code here`,
                                          mask   : '*',
                                          prefix : '   '
                                      })).code;
    }

    private getGitlabCodeFromGraphicEnvironment(): Promise<string> {
        return new Promise<string>((resolve, reject) => {
            ora({
                    text  : 'GUI mode (for headless mode, use the --cli option)',
                    indent: 4
                }).start().info();

            let currentSpinner: ora.Ora = ora({
                                                  text  : 'Starting login server',
                                                  indent: 4
                                              }).start();

            const loginServer = new LoginServer();
            loginServer.events.on('started', () => {
                currentSpinner.succeed('Login server started');
                currentSpinner = ora({
                                         text  : `Waiting for user to authorize the application in his web browser. If the browser does not open automatically, please go to : ${ Config.login.gitlab.url.code }`,
                                         indent: 4
                                     }).start();
                open(Config.login.gitlab.url.code).then();
            });
            loginServer.events.on('error', (error: string) => {
                currentSpinner.fail(`Login server error: ${ error }`);
                reject();
            });
            loginServer.events.on('stopped', () => {
                currentSpinner.succeed('Login server stopped');
            });


            loginServer.events.on('code', (code: string) => {
                currentSpinner.succeed('Login code received');
                currentSpinner = ora({
                                         text  : 'Stopping login server',
                                         indent: 4
                                     }).start();
                loginServer.events.on('stopped', () => {
                    resolve(code);
                });
                loginServer.stop();

                resolve(code);
            });

            loginServer.start();
        });
    }

    async login(headless: boolean = false) {
        try {
            this.logout();
        } catch ( error ) {
            console.log(error);
            ora('Unknown error').start().fail();
            throw error;
        }

        ora(`Login with Gitlab (${ SharedConfig.gitlab.URL }):`).start().info();

        let gitlabCode: string;
        if ( !headless ) {
            gitlabCode = await this.getGitlabCodeFromGraphicEnvironment();
        } else {
            gitlabCode = await this.getGitlabCodeFromHeadlessEnvironment();
        }

        const gitlabTokensSpinner = ora({
                                            text  : 'Retrieving gitlab tokens',
                                            indent: 4
                                        }).start();
        let gitlabTokens: GitlabToken;
        try {
            gitlabTokens = await GitlabManager.getTokens(gitlabCode);
            this.gitlabCredentials = {
                refreshToken: gitlabTokens.refresh_token,
                accessToken : gitlabTokens.access_token
            };
            gitlabTokensSpinner.succeed('Gitlab tokens retrieved');
        } catch ( error ) {
            gitlabTokensSpinner.fail('Error while retrieving gitlab tokens');
            throw error;
        }

        const isGitlabTokensValid = (await GitlabManager.testToken()).every(value => value);
        if ( !isGitlabTokensValid ) {
            throw new Error('Gitlab tokens are invalid');
        }

        ora(`Login to Dojo backend:`).start().info();
        const dojoLoginSpinner = ora({
                                         text  : 'Login to Dojo backend',
                                         indent: 4
                                     }).start();

        try {
            await DojoBackendManager.login(gitlabTokens);
            dojoLoginSpinner.succeed('Logged in');
        } catch ( error ) {
            dojoLoginSpinner.fail('Login failed');
            throw error;
        }

        await this.testSession(true);
    }

    async refreshTokens() {
        const gitlabTokens = await DojoBackendManager.refreshTokens(this.gitlabCredentials.refreshToken!);

        this.gitlabCredentials = {
            refreshToken: gitlabTokens.refresh_token,
            accessToken : gitlabTokens.access_token
        };
    }

    logout() {
        this.apiToken = '';
        this.gitlabCredentials = {
            refreshToken: '',
            accessToken : ''
        };
    }

    private hasPermission(permissionPredicate: () => boolean, verbose: boolean, verboseText: string, indent: number): boolean {
        const isAllowed: boolean = this.profile !== undefined && permissionPredicate();

        if ( verbose ) {
            const spinner: ora.Ora = ora({
                                             text  : verboseText,
                                             indent: indent
                                         }).start();
            isAllowed ? spinner.succeed() : spinner.fail();
        }

        return isAllowed;
    }

    checkPermissions(verbose: boolean = true, indent: number = 8, checkPermissions: Array<string> | null = []): Permissions {
        return {
            student      : checkPermissions && (checkPermissions.length === 0 || checkPermissions.includes('student')) ? this.hasPermission(() => true, verbose, 'Student permissions', indent) : false,
            teachingStaff: checkPermissions && (checkPermissions.length === 0 || checkPermissions.includes('teachingStaff')) ? this.hasPermission(() => this.profile?.isTeachingStaff ?? false, verbose, 'Teaching staff permissions', indent) : false,
            admin        : checkPermissions && (checkPermissions.length === 0 || checkPermissions.includes('admin')) ? this.hasPermission(() => this.profile?.isAdmin ?? false, verbose, 'Admin permissions', indent) : false
        };
    }


    async testSession(verbose: boolean = true, checkPermissions: Array<string> | null = []): Promise<false | Permissions> {
        if ( verbose ) {
            ora('Checking Dojo session: ').start().info();
        }

        HttpManager.handleAuthorizationCommandErrors = false;

        const spinner: ora.Ora = ora({
                                         text  : `Testing Dojo session`,
                                         indent: 4
                                     });
        if ( verbose ) {
            spinner.start();
        }

        try {
            await axios.get(DojoBackendHelper.getApiUrl(ApiRoute.TEST_SESSION), {});

            if ( verbose ) {
                spinner.succeed(`The session is valid`);
            }
        } catch ( error ) {
            if ( verbose ) {
                spinner.fail(`The session is invalid`);
            }

            return false;
        }

        if ( checkPermissions && checkPermissions.length === 0 ) {
            ora({
                    text  : `Here is your permissions:`,
                    indent: 4
                }).start().info();
        }

        return this.checkPermissions(verbose, 8, checkPermissions);
    }
}


export default new SessionManager();