import axios                    from 'axios';
import Config                   from '../config/Config';
import GitlabRepository         from '../shared/types/Gitlab/GitlabRepository';
import GitlabAccessLevel        from '../shared/types/Gitlab/GitlabAccessLevel';
import GitlabMember             from '../shared/types/Gitlab/GitlabMember';
import { StatusCodes }          from 'http-status-codes';
import GitlabVisibility         from '../shared/types/Gitlab/GitlabVisibility';
import GitlabUser               from '../shared/types/Gitlab/GitlabUser';
import GitlabTreeFile           from '../shared/types/Gitlab/GitlabTreeFile';
import parseLinkHeader          from 'parse-link-header';
import GitlabFile               from '../shared/types/Gitlab/GitlabFile';
import express                  from 'express';
import GitlabRoute              from '../shared/types/Gitlab/GitlabRoute';
import SharedConfig             from '../shared/config/SharedConfig';
import GitlabProfile            from '../shared/types/Gitlab/GitlabProfile';
import GitlabRelease            from '../shared/types/Gitlab/GitlabRelease';
import { CommitSchema, Gitlab } from '@gitbeaker/rest';
import logger                   from '../shared/logging/WinstonLogger';


class GitlabManager {
    readonly api = new Gitlab({
                                  host : SharedConfig.gitlab.URL,
                                  token: Config.gitlab.account.token
                              });

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

    public async getUserProfile(token: string): Promise<GitlabProfile | undefined> {
        try {
            return (await axios.get<GitlabProfile>(this.getApiUrl(GitlabRoute.PROFILE_GET), {
                headers: {
                    DojoOverrideAuthorization: true,
                    DojoAuthorizationHeader  : 'Authorization',
                    DojoAuthorizationValue   : `Bearer ${ token }`
                }
            })).data;
        } catch ( e ) {
            return undefined;
        }
    }

    public async getUserById(id: number): Promise<GitlabUser | undefined> {
        try {
            const user = (await axios.get<GitlabUser>(`${ this.getApiUrl(GitlabRoute.USERS_GET) }/${ String(id) }`)).data;

            return user.id === id ? user : undefined;
        } catch ( e ) {
            return undefined;
        }
    }

    public async getUserByUsername(username: string): Promise<GitlabUser | undefined> {
        try {
            const params: Record<string, string> = {};
            params['search'] = username;
            const user = (await axios.get<Array<GitlabUser>>(this.getApiUrl(GitlabRoute.USERS_GET), { params: params })).data[0];

            return user.username === username ? user : undefined;
        } catch ( e ) {
            return undefined;
        }
    }

    async getRepository(projectIdOrNamespace: string): Promise<GitlabRepository> {
        const response = await axios.get<GitlabRepository>(this.getApiUrl(GitlabRoute.REPOSITORY_GET).replace('{{id}}', encodeURIComponent(projectIdOrNamespace)));

        return response.data;
    }

    async getRepositoryMembers(idOrNamespace: string): Promise<Array<GitlabMember>> {
        const response = await axios.get<Array<GitlabMember>>(this.getApiUrl(GitlabRoute.REPOSITORY_MEMBERS_GET).replace('{{id}}', encodeURIComponent(idOrNamespace)));

        return response.data;
    }

    async getRepositoryReleases(repoId: number): Promise<Array<GitlabRelease>> {
        const response = await axios.get<Array<GitlabRelease>>(this.getApiUrl(GitlabRoute.REPOSITORY_RELEASES_GET).replace('{{id}}', String(repoId)));

        return response.data;
    }

    async getRepositoryLastCommit(repoId: number, branch: string = 'main'): Promise<CommitSchema | undefined> {
        try {
            const commits = await this.api.Commits.all(repoId, {
                refName : branch,
                maxPages: 1,
                perPage : 1
            });

            return commits.length > 0 ? commits[0] : undefined;
        } catch ( e ) {
            logger.error(e);
            return undefined;
        }
    }

    async createRepository(name: string, description: string, visibility: string, initializeWithReadme: boolean, namespace: number, sharedRunnersEnabled: boolean, wikiEnabled: boolean, import_url: string): Promise<GitlabRepository> {
        const response = await axios.post<GitlabRepository>(this.getApiUrl(GitlabRoute.REPOSITORY_CREATE), {
            name                  : name,
            description           : description,
            import_url            : import_url,
            initialize_with_readme: initializeWithReadme,
            namespace_id          : namespace,
            shared_runners_enabled: sharedRunnersEnabled,
            visibility            : visibility,
            wiki_enabled          : wikiEnabled
        });

        return response.data;
    }

    async deleteRepository(repoId: number): Promise<void> {
        return await axios.delete(this.getApiUrl(GitlabRoute.REPOSITORY_DELETE).replace('{{id}}', String(repoId)));
    }

    async forkRepository(forkId: number, name: string, path: string, description: string, visibility: string, namespace: number): Promise<GitlabRepository> {
        const response = await axios.post<GitlabRepository>(this.getApiUrl(GitlabRoute.REPOSITORY_FORK).replace('{{id}}', String(forkId)), {
            name        : name,
            path        : path,
            description : description,
            namespace_id: namespace,
            visibility  : visibility
        });

        return response.data;
    }

    async editRepository(repoId: number, newAttributes: Partial<GitlabRepository>): Promise<GitlabRepository> {
        const response = await axios.put<GitlabRepository>(this.getApiUrl(GitlabRoute.REPOSITORY_EDIT).replace('{{id}}', String(repoId)), newAttributes);

        return response.data;
    }

    async changeRepositoryVisibility(repoId: number, visibility: GitlabVisibility): Promise<GitlabRepository> {
        return await this.editRepository(repoId, { visibility: visibility.toString() });
    }

    async addRepositoryMember(repoId: number, userId: number, accessLevel: GitlabAccessLevel): Promise<GitlabMember> {
        const response = await axios.post<GitlabMember>(this.getApiUrl(GitlabRoute.REPOSITORY_MEMBER_ADD).replace('{{id}}', String(repoId)), {
            user_id     : userId,
            access_level: accessLevel
        });

        return response.data;
    }

    async addRepositoryVariable(repoId: number, key: string, value: string, isProtected: boolean, isMasked: boolean): Promise<GitlabMember> {
        const response = await axios.post<GitlabMember>(this.getApiUrl(GitlabRoute.REPOSITORY_VARIABLES_ADD).replace('{{id}}', String(repoId)), {
            key          : key,
            variable_type: 'env_var',
            value        : value,
            protected    : isProtected,
            masked       : isMasked
        });

        return response.data;
    }

    async addRepositoryBadge(repoId: number, linkUrl: string, imageUrl: string, name: string): Promise<GitlabMember> {
        const response = await axios.post<GitlabMember>(this.getApiUrl(GitlabRoute.REPOSITORY_BADGES_ADD).replace('{{id}}', String(repoId)), {
            link_url : linkUrl,
            image_url: imageUrl,
            name     : name
        });

        return response.data;
    }

    async checkTemplateAccess(projectIdOrNamespace: string, req: express.Request): Promise<StatusCodes> {
        // Get the Gitlab project and check if it have public or internal visibility
        try {
            const project: GitlabRepository = await this.getRepository(projectIdOrNamespace);

            if ( [ GitlabVisibility.PUBLIC.valueOf(), GitlabVisibility.INTERNAL.valueOf() ].includes(project.visibility) ) {
                return StatusCodes.OK;
            }
        } catch ( e ) {
            return StatusCodes.NOT_FOUND;
        }

        // Check if the user and dojo are members (with at least reporter access) of the project
        const members = await this.getRepositoryMembers(projectIdOrNamespace);
        const isUsersAtLeastReporter = {
            user: false,
            dojo: false
        };
        members.forEach(member => {
            if ( member.access_level >= GitlabAccessLevel.REPORTER ) {
                if ( member.id === req.session.profile.id ) {
                    isUsersAtLeastReporter.user = true;
                } else if ( member.id === Config.gitlab.account.id ) {
                    isUsersAtLeastReporter.dojo = true;
                }
            }
        });

        return isUsersAtLeastReporter.user && isUsersAtLeastReporter.dojo ? StatusCodes.OK : StatusCodes.UNAUTHORIZED;
    }

    async protectBranch(repoId: number, branchName: string, allowForcePush: boolean, allowedToMerge: GitlabAccessLevel, allowedToPush: GitlabAccessLevel, allowedToUnprotect: GitlabAccessLevel): Promise<GitlabMember> {
        const response = await axios.post<GitlabMember>(this.getApiUrl(GitlabRoute.REPOSITORY_BRANCHES_PROTECT).replace('{{id}}', String(repoId)), {
            name                  : branchName,
            allow_force_push      : allowForcePush,
            merge_access_level    : allowedToMerge.valueOf(),
            push_access_level     : allowedToPush.valueOf(),
            unprotect_access_level: allowedToUnprotect.valueOf()
        });

        return response.data;
    }

    async getRepositoryTree(repoId: number, recursive: boolean = true, branch: string = 'main'): Promise<Array<GitlabTreeFile>> {
        const address: string | undefined = this.getApiUrl(GitlabRoute.REPOSITORY_TREE).replace('{{id}}', String(repoId));
        let params: Partial<parseLinkHeader.Link | { recursive: boolean, per_page: number }> | undefined = {
            pagination: 'keyset',
            recursive : recursive,
            per_page  : 100,
            ref       : branch
        };

        const results: Array<GitlabTreeFile> = [];

        while ( params !== undefined ) {
            const response = await axios.get<Array<GitlabTreeFile>>(address, {
                params: params
            });

            results.push(...response.data);

            if ( 'link' in response.headers ) {
                params = parseLinkHeader(response.headers['link'])?.next ?? undefined;
            } else {
                params = undefined;
            }
        }

        return results;
    }

    async getFile(repoId: number, filePath: string, branch: string = 'main'): Promise<GitlabFile> {
        const response = await axios.get<GitlabFile>(this.getApiUrl(GitlabRoute.REPOSITORY_FILE).replace('{{id}}', String(repoId)).replace('{{filePath}}', encodeURIComponent(filePath)), {
            params: {
                ref: branch
            }
        });

        return response.data;
    }

    private async createUpdateFile(create: boolean, repoId: number, filePath: string, fileBase64: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined) {
        const axiosFunction = create ? axios.post : axios.put;

        await axiosFunction(this.getApiUrl(GitlabRoute.REPOSITORY_FILE).replace('{{id}}', String(repoId)).replace('{{filePath}}', encodeURIComponent(filePath)), {
            encoding      : 'base64',
            branch        : branch,
            commit_message: commitMessage,
            content       : fileBase64,
            author_name   : authorName,
            author_email  : authorMail
        });
    }

    async createFile(repoId: number, filePath: string, fileBase64: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined) {
        return this.createUpdateFile(true, repoId, filePath, fileBase64, commitMessage, branch, authorName, authorMail);
    }

    async updateFile(repoId: number, filePath: string, fileBase64: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined) {
        return this.createUpdateFile(false, repoId, filePath, fileBase64, commitMessage, branch, authorName, authorMail);
    }

    async deleteFile(repoId: number, filePath: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined) {
        await axios.delete(this.getApiUrl(GitlabRoute.REPOSITORY_FILE).replace('{{id}}', String(repoId)).replace('{{filePath}}', encodeURIComponent(filePath)), {
            data: {
                branch        : branch,
                commit_message: commitMessage,
                author_name   : authorName,
                author_email  : authorMail
            }
        });

    }
}


export default new GitlabManager();