import axios            from 'axios';
import ora              from 'ora';
import GitlabUser       from '../shared/types/Gitlab/GitlabUser';
import GitlabRoute      from '../shared/types/Gitlab/GitlabRoute';
import SharedConfig     from '../shared/config/SharedConfig';
import GitlabRepository from '../shared/types/Gitlab/GitlabRepository';
import fs               from 'fs-extra';
import { spawn }        from 'child_process';


class GitlabManager {
    private getApiUrl(route: GitlabRoute): string {
        return `${ SharedConfig.gitlab.apiURL }${ route }`;
    }

    public async testToken(verbose: boolean = true): Promise<[ boolean, boolean ]> {
        if ( verbose ) {
            ora('Checking Gitlab token: ').start().info();
        }

        const result: [ boolean, boolean ] = [ false, false ];

        type NotificationSettings = { level: string }

        let notificationSettings: NotificationSettings = { level: 'error' };

        // Test read access
        {
            const spinnerRead: ora.Ora = ora({
                                                 text  : `Read access`,
                                                 indent: 4
                                             });
            if ( verbose ) {
                spinnerRead.start();
            }

            try {
                notificationSettings = (await this.getNotificationSettings()).data as NotificationSettings;

                result[0] = true;

                if ( verbose ) {
                    spinnerRead.succeed();
                }
            } catch ( e ) {
                if ( verbose ) {
                    spinnerRead.fail();
                }
            }
        }

        // Test write access
        {
            const spinnerWrite: ora.Ora = ora({
                                                  text  : `Write access`,
                                                  indent: 4
                                              });
            if ( verbose ) {
                spinnerWrite.start();
            }

            const someLevelTypes = [ 'disabled', 'participating' ];

            try {
                const oldSettings = notificationSettings;
                const newSettings = { level: someLevelTypes[someLevelTypes[0] == oldSettings.level ? 1 : 0] };

                await this.setNotificationSettings(newSettings);
                await this.setNotificationSettings(oldSettings);

                result[1] = true;

                if ( verbose ) {
                    spinnerWrite.succeed();
                }
            } catch ( e ) {
                if ( verbose ) {
                    spinnerWrite.fail();
                }
            }
        }

        return result;
    }

    public getNotificationSettings() {
        return axios.get(this.getApiUrl(GitlabRoute.NOTIFICATION_SETTINGS));
    }

    public setNotificationSettings(newSettings: Record<string, string>) {
        return axios.put(this.getApiUrl(GitlabRoute.NOTIFICATION_SETTINGS), { params: new URLSearchParams(newSettings) });
    }

    private async getGitlabUsers(paramsToSearch: Array<string | number>, paramName: string, verbose: boolean = false, verboseIndent: number = 0): Promise<Array<GitlabUser | undefined>> {
        try {
            return await Promise.all(paramsToSearch.map(async param => {
                const spinner: ora.Ora = ora({
                                                 text  : `Getting user ${ param }`,
                                                 indent: verboseIndent
                                             });
                if ( verbose ) {
                    spinner.start();
                }
                const params: { [key: string]: unknown } = {};
                params[paramName] = param;
                const user = await axios.get<Array<GitlabUser>>(this.getApiUrl(GitlabRoute.USERS_GET), { params: params });

                if ( user.data[0] ) {
                    const gitlabUser = user.data[0];

                    if ( verbose ) {
                        spinner.succeed(`${ gitlabUser.username } (${ gitlabUser.id })`);
                    }
                    return gitlabUser;
                } else {
                    if ( verbose ) {
                        spinner.fail(`${ param }`);
                    }
                }
            }));
        } catch ( e ) {
            return [ undefined ];
        }
    }

    public async getUsersById(ids: Array<number>, verbose: boolean = false, verboseIndent: number = 0): Promise<Array<GitlabUser | undefined>> {
        return await this.getGitlabUsers(ids, 'id', verbose, verboseIndent);
    }

    public async getUsersByUsername(usernames: Array<string>, verbose: boolean = false, verboseIndent: number = 0): Promise<Array<GitlabUser | undefined>> {
        return await this.getGitlabUsers(usernames, 'search', verbose, verboseIndent);
    }

    public async getRepository(repoId: number): Promise<GitlabRepository> {
        return await axios.get(this.getApiUrl(GitlabRoute.REPOSITORY_GET).replace('{{id}}', repoId.toString()));
    }

    public async fetchMembers(options: { members_id?: Array<number>, members_username?: Array<string> }): Promise<Array<GitlabUser> | false> {
        if ( options.members_id || options.members_username ) {
            ora('Checking Gitlab members:').start().info();
        }

        let members: Array<GitlabUser> = [];

        async function getMembers<T>(context: unknown, functionName: string, paramsToSearch: Array<T>): Promise<boolean> {
            const result = await ((context as { [functionName: string]: (arg: Array<T>, verbose: boolean, verboseIndent: number) => Promise<Array<GitlabUser | undefined>> })[functionName])(paramsToSearch, true, 8);

            if ( result.every(user => user) ) {
                members = members.concat(result as Array<GitlabUser>);
                return true;
            } else {
                return false;
            }
        }

        let result = true;

        if ( options.members_id ) {
            ora({
                    text  : 'Fetching members by id:',
                    indent: 4
                }).start().info();

            result = await getMembers(this, 'getUsersById', options.members_id);
        }

        if ( options.members_username ) {
            ora({
                    text  : 'Fetching members by username:',
                    indent: 4
                }).start().info();

            result = result && await getMembers(this, 'getUsersByUsername', options.members_username);
        }

        if ( !result ) {
            return false;
        }

        members = members.removeObjectDuplicates(gitlabUser => gitlabUser.id);

        return members;
    }

    public async cloneRepository(clonePath: string | boolean, repositorySshUrl: string, folderName?: string, verbose: boolean = false, verboseIndent: number = 0) {
        let path = './';
        if ( typeof clonePath === 'string' ) {
            path = clonePath;

            fs.mkdirSync(path, { recursive: true });
        }

        let cloningSpinner!: ora.Ora;
        if ( verbose ) {
            cloningSpinner = ora({
                                     text  : 'Cloning the repository...',
                                     indent: verboseIndent
                                 }).start();
        }

        try {
            await new Promise<void>((resolve, reject) => {
                const gitClone = spawn(`git clone ${ repositorySshUrl } "${ folderName ?? '' }"`, {
                    cwd  : path,
                    shell: true
                });

                gitClone.on('exit', (code) => {
                    code !== null && code == 0 ? resolve() : reject();
                });
            });

            if ( verbose ) {
                cloningSpinner.succeed('Repository cloned');
            }
        } catch ( error ) {
            if ( verbose ) {
                cloningSpinner.fail('Error while cloning the repository');
            }
        }
    }
}


export default new GitlabManager();