import Config                                                                                                                                                                                 from '../config/Config.js';
import { StatusCodes }                                                                                                                                                                        from 'http-status-codes';
import GitlabVisibility                                                                                                                                                                       from '../shared/types/Gitlab/GitlabVisibility.js';
import express                                                                                                                                                                                from 'express';
import { CommitSchema, ExpandedUserSchema, Gitlab, MemberSchema, ProjectBadgeSchema, ProjectSchema, ReleaseSchema, RepositoryFileExpandedSchema, RepositoryFileSchema, RepositoryTreeSchema } from '@gitbeaker/rest';
import logger                                                                                                                                                                                 from '../shared/logging/WinstonLogger.js';
import { AccessLevel, EditProjectOptions, ProjectVariableSchema, ProtectedBranchAccessLevel, ProtectedBranchSchema }                                                                          from '@gitbeaker/core';
import DojoStatusCode                                                                                                                                                                         from '../shared/types/Dojo/DojoStatusCode.js';
import SharedGitlabManager                                                                                                                                                                    from '../shared/managers/SharedGitlabManager.js';


class GitlabManager extends SharedGitlabManager {
    constructor() {
        super(Config.gitlab.url, Config.gitlab.account.token);
    }

    getUserProfile(token: string): Promise<ExpandedUserSchema> | undefined {
        try {
            const profileApi = new Gitlab({
                                              host      : Config.gitlab.url,
                                              oauthToken: token
                                          });

            return profileApi.Users.showCurrentUser();
        } catch ( e ) {
            logger.error(JSON.stringify(e));
            return undefined;
        }
    }

    async getRepositoryMembers(idOrNamespace: string, includeInherited: boolean = true): Promise<Array<MemberSchema>> {
        try {
            return await this.api.ProjectMembers.all(idOrNamespace, { includeInherited: includeInherited });
        } catch ( e ) {
            logger.error(JSON.stringify(e));
            return Promise.reject(e);
        }
    }

    async getRepositoryReleases(repoId: number): Promise<Array<ReleaseSchema>> {
        try {
            return await this.api.ProjectReleases.all(repoId);
        } catch ( e ) {
            logger.error(JSON.stringify(e));
            return Promise.reject(e);
        }
    }

    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(JSON.stringify(e));
            return undefined;
        }
    }

    async getRepositoryCommit(repoId: number, commitSha: string): Promise<CommitSchema | undefined> {
        try {
            return await this.api.Commits.show(repoId, commitSha);
        } catch ( e ) {
            logger.error(JSON.stringify(e));
            return undefined;
        }
    }

    async createRepository(name: string, description: string, visibility: 'public' | 'internal' | 'private', initializeWithReadme: boolean, namespace: number, sharedRunnersEnabled: boolean, wikiEnabled: boolean, importUrl: string): Promise<ProjectSchema> {
        try {
            return await this.api.Projects.create({
                                                      name                : name,
                                                      description         : description,
                                                      importUrl           : importUrl,
                                                      initializeWithReadme: initializeWithReadme,
                                                      namespaceId         : namespace,
                                                      sharedRunnersEnabled: sharedRunnersEnabled,
                                                      visibility          : visibility,
                                                      wikiAccessLevel     : wikiEnabled ? 'enabled' : 'disabled'
                                                  });
        } catch ( e ) {
            logger.error(JSON.stringify(e));
            return Promise.reject(e);
        }
    }

    async deleteRepository(repoId: number): Promise<void> {
        try {
            return await this.api.Projects.remove(repoId);
        } catch ( e ) {
            logger.error(JSON.stringify(e));
            return Promise.reject(e);
        }
    }

    async forkRepository(forkId: number, name: string, path: string, description: string, visibility: 'public' | 'internal' | 'private', namespace: number): Promise<ProjectSchema> {
        try {
            return await this.api.Projects.fork(forkId, {
                name       : name,
                path       : path,
                description: description,
                namespaceId: namespace,
                visibility : visibility
            });
        } catch ( e ) {
            logger.error(JSON.stringify(e));
            return Promise.reject(e);
        }
    }

    async editRepository(repoId: number, newAttributes: EditProjectOptions): Promise<ProjectSchema> {
        try {
            return await this.api.Projects.edit(repoId, newAttributes);
        } catch ( e ) {
            logger.error(JSON.stringify(e));
            return Promise.reject(e);
        }
    }

    async deleteRepositoryMember(repoId: number, userId: number, skipSubresources: boolean = false, unassignIssuables: boolean = false): Promise<void> {
        try {
            return await this.api.ProjectMembers.remove(repoId, userId, {
                skipSubresourceS: skipSubresources,
                unassignIssuables
            });
        } catch ( e ) {
            logger.error(JSON.stringify(e));
            return Promise.reject(e);
        }
    }

    async renameRepository(repoId: number, newName: string): Promise<ProjectSchema> {
        try {
            return await this.api.Projects.edit(repoId, {
                name: newName
            });
        } catch ( e ) {
            logger.error(JSON.stringify(e));
            return Promise.reject(e);
        }
    }

    async moveRepository(repoId: number, newRepoId: number): Promise<ProjectSchema> {
        try {
            return await this.api.Projects.transfer(repoId, newRepoId);
        } catch ( e ) {
            logger.error(JSON.stringify(e));
            return Promise.reject(e);
        }
    }

    changeRepositoryVisibility(repoId: number, visibility: GitlabVisibility): Promise<ProjectSchema> {
        return this.editRepository(repoId, { visibility: visibility });
    }

    async addRepositoryMember(repoId: number, userId: number, accessLevel: Exclude<AccessLevel, AccessLevel.ADMIN>): Promise<MemberSchema> {
        try {
            return await this.api.ProjectMembers.add(repoId, accessLevel, { userId });
        } catch ( e ) {
            logger.error(JSON.stringify(e));
            return Promise.reject(e);
        }
    }

    async addRepositoryVariable(repoId: number, key: string, value: string, isProtected: boolean, isMasked: boolean): Promise<ProjectVariableSchema> {
        try {
            return await this.api.ProjectVariables.create(repoId, key, value, {
                variableType: 'env_var',
                protected   : isProtected,
                masked      : isMasked
            });
        } catch ( e ) {
            logger.error(JSON.stringify(e));
            return Promise.reject(e);
        }
    }

    async addRepositoryBadge(repoId: number, linkUrl: string, imageUrl: string, name: string): Promise<ProjectBadgeSchema> {
        try {
            return await this.api.ProjectBadges.add(repoId, linkUrl, imageUrl, {
                name: name
            });
        } catch ( e ) {
            logger.error(JSON.stringify(e));
            return Promise.reject(e);
        }
    }

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

            if ( [ 'public', 'internal' ].includes(project.visibility) ) {
                req.session.sendResponse(res, StatusCodes.OK);
                return true;
            }
        } catch ( e ) {
            req.session.sendResponse(res, StatusCodes.NOT_FOUND, undefined, 'Template not found', DojoStatusCode.GITLAB_TEMPLATE_NOT_FOUND);
            return false;
        }

        // 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 >= AccessLevel.REPORTER ) {
                if ( member.id === req.session.profile.id ) {
                    isUsersAtLeastReporter.user = true;
                } else if ( member.id === Config.gitlab.account.id ) {
                    isUsersAtLeastReporter.dojo = true;
                }
            }
        });

        if ( isUsersAtLeastReporter.user && isUsersAtLeastReporter.dojo ) {
            req.session.sendResponse(res, StatusCodes.OK);
            return true;
        } else {
            req.session.sendResponse(res, StatusCodes.UNAUTHORIZED, undefined, 'Template access unauthorized', DojoStatusCode.GITLAB_TEMPLATE_ACCESS_UNAUTHORIZED);
            return false;
        }
    }

    async protectBranch(repoId: number, branchName: string, allowForcePush: boolean, allowedToMerge: ProtectedBranchAccessLevel, allowedToPush: ProtectedBranchAccessLevel, allowedToUnprotect: ProtectedBranchAccessLevel): Promise<ProtectedBranchSchema> {
        try {
            return await this.api.ProtectedBranches.protect(repoId, branchName, {
                allowForcePush      : allowForcePush,
                mergeAccessLevel    : allowedToMerge,
                pushAccessLevel     : allowedToPush,
                unprotectAccessLevel: allowedToUnprotect
            });
        } catch ( e ) {
            logger.error(JSON.stringify(e));
            return Promise.reject(e);
        }
    }

    async getRepositoryTree(repoId: number, recursive: boolean = true, branch: string = 'main'): Promise<Array<RepositoryTreeSchema>> {
        try {
            return await this.api.Repositories.allRepositoryTrees(repoId, {
                recursive: recursive,
                ref      : branch
            });
        } catch ( e ) {
            logger.error(JSON.stringify(e));
            return Promise.reject(e);
        }
    }

    async getFile(repoId: number, filePath: string, branch: string = 'main'): Promise<RepositoryFileExpandedSchema> {
        try {
            return await this.api.RepositoryFiles.show(repoId, filePath, branch);
        } catch ( e ) {
            logger.error(JSON.stringify(e));
            return Promise.reject(e);
        }
    }

    private async createUpdateFile(create: boolean, repoId: number, filePath: string, fileBase64: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined): Promise<RepositoryFileSchema> {
        try {
            const gitFunction = create ? this.api.RepositoryFiles.create.bind(this.api) : this.api.RepositoryFiles.edit.bind(this.api);

            return await gitFunction(repoId, filePath, branch, fileBase64, commitMessage, {
                encoding   : 'base64',
                authorName : authorName,
                authorEmail: authorMail
            });
        } catch ( e ) {
            logger.error(JSON.stringify(e));
            return Promise.reject(e);
        }
    }

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

    updateFile(repoId: number, filePath: string, fileBase64: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined): Promise<RepositoryFileSchema> {
        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): Promise<void> {
        try {
            return await this.api.RepositoryFiles.remove(repoId, filePath, branch, commitMessage, {
                authorName : authorName,
                authorEmail: authorMail
            });
        } catch ( e ) {
            logger.error(JSON.stringify(e));
            return Promise.reject(e);
        }
    }
}


export default new GitlabManager();