import { Option } from 'commander';
import { createHash, randomBytes } from 'crypto';
import express, { response } from 'express';
import open from 'open';
import { PrettyPrint } from '../output/pretty_print.js';

export class Authentication {
    constructor(program, logger, services) {
        this.logger = logger;
        this.services = services;
        this.configManager = services["configManager"];
        this.rest = services["rest"];
        services["authentication"] = this;

        this.logger.debug('Authentication.constructor');

        program
            .command('authent')
            .argument('[uid]', 'login email, loginphone, external user identifier (SSO)')
            .description('get authentication urls for user')
            .addOption(new Option('--country', 'country if uid is a phone number'))
            //            .addOption(new Option('--usebackchannelpolling', 'use back channel polling (SSO)'))
            .action(async (uid, options) => {
                PrettyPrint.withSpinner(
                    async () => { return await this.getAuthenticationUrls(uid, options); },
                    (json) => { PrettyPrint.display(json); }
                );
            });

        const apikeyCommand = program
            .command('apikey')
            .description('display current API key')
            .action(async (options) => {
                PrettyPrint.withSpinner(
                    async () => { return await this.getCurrentApikey(options); },
                    (json) => { PrettyPrint.display(json); }
                );
            });

        apikeyCommand
            .command('create')
            .argument('<description>', 'API key description')
            .description('create a new API key')
            .addOption(new Option('-s, --scope <scope>', 'API key scope (default : all)'))
            .addOption(new Option('--inactive', 'new API key is inactive'))
            .addOption(new Option('--expiration <date>', 'API key expiration date'))
            .action(async (description, options) => {
                PrettyPrint.withSpinner(
                    async () => { return await this.generateApiKey(description, options); },
                    (json) => { PrettyPrint.display(json); }
                );
            });

        apikeyCommand
            .command('delete')
            .argument('<apikeyid>', 'id of API key to delete')
            .description('delete api key with id')
            .action(async (apikeyid, options) => {
                PrettyPrint.withSpinner(
                    async () => { return await this.deleteApiKey(apikeyid, options); },
                    (json) => { PrettyPrint.display(json); }
                );
            });

        program
            .command('apikeys')
            .description('get api keys')
            .action(async (options) => {
                PrettyPrint.withSpinner(
                    async () => { return await this.getAllApiKeys(options); },
                    (json) => { PrettyPrint.display(json); }
                );
            });

//        program
//            .command('login')
//            .description('login user')
//            .action(); // TODO
    }

    async login(options, credentials) {
        this.logger.debug('Authentication.login');

        const authMethod = credentials.authMethod;
        switch (authMethod) {
            case "RAINBOW":
                try {
                    return await this.loginRainbow(options, credentials);
                }
                catch (error) {
                    this.logger.error(error);
                    throw error;
                }
            case "OAUTH":
                try {
                    return await this.loginOAuth(options, credentials);
                }
                catch (error) {
                    this.logger.error(error);
                    throw error;
                }
            case "API_KEY":
                this.logger.warn('login not needed when using API_KEY authentication method');
                return;
            default:
                this.logger.error(`authentication method ${authMethod} is not supported`);
                throw (`${authMethod} authentication method not supported`);
        }
    }

    async loginRainbow(options, credentials) {
        this.logger.debug('Authentication.loginRainbow');

        try {
            const response = await this.rest.get('/api/rainbow/authentication/v1.0/login', [], this.#getAppAuth(credentials));
            if (response.status !== 200) {
                const error_details = await response.json();
                this.logger.error(`error: ${response.status} - ${response.statusText} - ${error_details.errorDetailsCode}`);
                throw `status ${response.status}`;
            }
            let json_response = await response.json();
            // Manage MFA
            if (json_response.mfaRequired === true) {
                this.logger.debug('MFA required');

                // Get MFA validation code from the user
                const mfaCode = await this.#getMFAValidationCode();

                // Get MFA validation URL
                const mfaUrl = json_response.mfaValidationUrl;
                this.logger.debug(`MFA validation URL: ${mfaUrl}`);
                const tempToken = json_response.token;
                const responseMFA = await this.rest.mfaValidationRequest(mfaUrl, mfaCode, tempToken);
                if (responseMFA.status !== 200) {
                    const error_details = await responseMFA.json();
                    this.logger.error(`MFA validation error: ${responseMFA.status} - ${responseMFA.statusText} - ${error_details.errorDetailsCode}`);
                    throw `status ${responseMFA.status}`;
                }
                this.logger.debug('MFA validation successful');
                json_response = await responseMFA.json();
            }
            this.logger.debug(`login response: ${JSON.stringify(json_response)}`);

            // TODO: do we have expires_in ?
            return json_response;
        }
        catch (error) {
            this.logger.error(error);
            throw error;
        }
    }

    async loginOAuth(options, credentials) {
        this.logger.debug('Authentication.loginOAuth');

        // Tenter le refresh si on a un refresh_token
        if (credentials.refreshToken) {
            try {
                this.logger.debug('Attempting OAuth token refresh');
                return await this.refreshOAuthToken(credentials);
            } catch (error) {
                this.logger.debug('Refresh token failed, falling back to full OAuth flow');
            }
        }

        const app = express();

        const state = randomBytes(20).toString('hex');

        let resolve;
        const p = new Promise((_resolve) => {
            resolve = _resolve;
        });
        app.get('/oauth', (req, res) => {
            this.logger.debug(`received /oauth request: state: ${req.query.state}`)
            if (state !== req.query.state) {
                this.logger.error('incorrect state !');
            }
            resolve(req.query.code);
            res.end('');
        });
        const server = app.listen(3000);

        const redirect_uri = encodeURIComponent('http://localhost:3000/oauth');
        open(`https://${credentials.host}/api/rainbow/authentication/v1.0/oauth/authorize?response_type=code&client_id=${credentials.applicationId}&redirect_uri=${redirect_uri}&scope=all&state=${state}`)

        const code = await p;
        this.logger.debug(`code: ${code}`);

        const app_auth = 'Basic ' + Buffer.from(credentials.applicationId + ':' + credentials.applicationSecret).toString('base64');
        const init = {
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
                'Authorization': app_auth
            },
            method: 'POST',
            body: `grant_type=authorization_code&code=${code}&redirect_uri=${redirect_uri}`
        };
        const response = await fetch(this.rest.urlBuilder(credentials.host, '/api/rainbow/authentication/v1.0/oauth/token'), init);
        if (response.status !== 200) {
            const error_details = await response.json();
            this.logger.error(`error: ${response.status} - ${response.statusText} - ${error_details.errorDetailsCode}`);
            throw `status ${response.status}`;
        }
        // TODO check content-type : text => response.text(), json => response.json(), manage errors (401500)
        const json_response = await response.json();

        server.close();

        const creation_date = Math.round(+new Date() / 1000);
        const expiration_date = creation_date + (json_response.expires_in || 0);

        return {
            'token': json_response.access_token,
            'refresh_token': json_response.refresh_token,
            'expiration_date': expiration_date,
            'creation_date': creation_date
        };
    }

    async refreshOAuthToken(credentials) {
        this.logger.debug('Authentication.refreshOAuthToken');

        const app_auth = 'Basic ' + Buffer.from(credentials.applicationId + ':' + credentials.applicationSecret).toString('base64');
        const response = await fetch(
            this.rest.urlBuilder(credentials.host, '/api/rainbow/authentication/v1.0/oauth/token'),
            {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'Authorization': app_auth
                },
                body: `grant_type=refresh_token&refresh_token=${credentials.refreshToken}`
            }
        );

        if (response.status !== 200) {
            throw new Error(`OAuth token refresh failed with status ${response.status}`);
        }

        const json_response = await response.json();
        const creation_date = Math.round(+new Date() / 1000);
        const expiration_date = creation_date + (json_response.expires_in || 0);

        return {
            'token': json_response.access_token,
            'refresh_token': json_response.refresh_token,
            'expiration_date': expiration_date,
            'creation_date': creation_date
        };
    }

    #getAppAuth(credentials) {
        this.logger.debug('Authentication.getAppAuth');

        const loginEmail = credentials.loginEmail;
        const password = credentials.password;
        const app_id = credentials.applicationId;
        const app_secret = credentials.applicationSecret;

        const authorization = 'Basic ' + Buffer.from(loginEmail + ':' + password).toString('base64');
        const encrypted_app_token = createHash('sha256').update(app_secret + password).digest('hex');
        const app_auth = 'Basic ' + Buffer.from(app_id + ':' + encrypted_app_token).toString('base64');

        return {
            'x-rainbow-app-auth': app_auth,
            'authorization': authorization
        };
    }

    async #getMFAValidationCode() {
        this.logger.debug('Authentication.getMFAValidationCode');

        const spinnerWasSpinning = PrettyPrint.spinner.isSpinning;
        if (spinnerWasSpinning) {
            PrettyPrint.spinner.stop();
        }
        const { default: input } = await import('@inquirer/input');
        const MFAValidationCode = await input({
            message: 'Enter MFA validation code:',
            validate: (value) => {
                if (value.length === 0) {
                    return 'MFA validation code cannot be empty';
                }
                return true;
            }
        });
        if (spinnerWasSpinning) {
            PrettyPrint.spinner.start();
        }
        return MFAValidationCode;
    }

    async getAuthenticationUrls(uid) {
        this.logger.debug('Authentication.getAuthenticationUrls');

        if (uid === undefined) {
            uid = await this.services["enduser"].myUserId();
        }
        const query_parameters = [
            ['uid', uid]
        ];

        const json = await this.services["rest"].restCall('GET', '/api/rainbow/authentication/v1.0/urls', query_parameters);
        return json;
    }

    async getCurrentApikey(options) {
        this.logger.debug('Authentication.getCurrentApiKey');

        const json = await this.services["rest"].restCall('GET', '/api/rainbow/authentication/v1.0/apikeys/current');
        return json;
    }

    // API KEYS
    // API Key APIs cannot be called using API key authent

    async getAllApiKeys(options) {
        this.logger.debug('Authentication.getAllApiKeys');

        const query_parameters = [];

        // isActive
        // fromCreationDate
        // toCreationDate
        // limit
        // offset
        // sortField
        // sortOrder
        // format
        // userId

        const json = await this.services["rest"].restCall('GET', '/api/rainbow/authentication/v1.0/apikeys');
        return json;
    }

    async generateApiKey(description, options) {
        this.logger.debug('Authentication.generateApiKey');

        const query_parameters = [];
        const body = {
            'scope': ['all'],
            'description': description,
            'isActive': true,
            //'expirationDate': 
        };

        const json = await this.services["rest"].restCall('POST', '/api/rainbow/authentication/v1.0/apikeys', query_parameters, body);
        return json;
    }

    async deleteApiKey(apikeyid, options) {
        this.logger.debug('Authentication.deleteApiKey');

        // TODO confirmation

        try {
            const json = await this.services['rest'].restCall('DELETE', `/api/rainbow/authentication/v1.0/apikeys/${apikeyid}`);
            return json;
        }
        catch (error) {
            this.logger.error(error);
            throw error;
        }
    }


    async getValidToken(profileName) {
        this.logger.debug('Authentication.getValidToken');

        const token = await this.configManager.getToken(profileName);

        if (token) {
            const decoded = jwtDecode(token);
            const now = Math.floor(Date.now() / 1000);

            // On vérifie si le token est encore valide (avec une marge de 60s)
            if (decoded.exp && decoded.exp > (now + 60)) {
                return token; // Le token est valide, on l'utilise
            }
            console.log("Token expired, renewal ongoing...");
        }

        // Si pas de token ou expiré, on utilise les credentials pour en obtenir un nouveau
        const creds = await this.configManager.getFullCredentials();
        if (!creds) throw new Error("Required authentication. Launch 'rainbow-cli login'");

        try {
            const loginResult = await this.login({ 'nooutput': true }, creds);
            const newToken = loginResult.token;
            await this.configManager.setToken(profileName, newToken);
            return newToken;
        }
        catch (error) {
            this.logger.error(error);
        }
        return null;
    }

}