import { Express }               from 'express-serve-static-core';
import express                   from 'express';
import * as ExpressValidator     from 'express-validator';
import { StatusCodes }           from 'http-status-codes';
import RoutesManager             from '../express/RoutesManager';
import ParamsValidatorMiddleware from '../middlewares/ParamsValidatorMiddleware';
import SecurityMiddleware        from '../middlewares/SecurityMiddleware';
import SecurityCheckType         from '../types/SecurityCheckType';
import GitlabManager             from '../managers/GitlabManager';
import Config                    from '../config/Config';
import logger                    from '../shared/logging/WinstonLogger';
import DojoValidators            from '../helpers/DojoValidators';
import { Prisma }                from '@prisma/client';
import db                        from '../helpers/DatabaseHelper';
import { Assignment }            from '../types/DatabaseTypes';
import AssignmentManager         from '../managers/AssignmentManager';
import fs                        from 'fs';
import path                      from 'path';
import SharedAssignmentHelper    from '../shared/helpers/Dojo/SharedAssignmentHelper';
import GlobalHelper              from '../helpers/GlobalHelper';
import DojoStatusCode            from '../shared/types/Dojo/DojoStatusCode';
import DojoModelsHelper          from '../helpers/DojoModelsHelper';
import * as Gitlab               from '@gitbeaker/rest';
import { GitbeakerRequestError } from '@gitbeaker/requester-utils';


class AssignmentRoutes implements RoutesManager {
    private readonly assignmentValidator: ExpressValidator.Schema = {
        name    : {
            trim    : true,
            notEmpty: true
        },
        members : {
            trim           : true,
            notEmpty       : true,
            customSanitizer: DojoValidators.jsonSanitizer
        },
        template: {
            trim           : true,
            custom         : DojoValidators.templateUrlValidator,
            customSanitizer: DojoValidators.templateUrlSanitizer
        }
    };

    private readonly assignmentAddCorrigeValidator: ExpressValidator.Schema = {
        exerciseIdOrUrl: {
            trim    : true,
            notEmpty: true,
            custom  : DojoValidators.exerciseIdOrUrlValidator
        }
    };

    registerOnBackend(backend: Express) {
        backend.get('/assignments/:assignmentNameOrUrl', SecurityMiddleware.check(true), this.getAssignment.bind(this));
        backend.post('/assignments', SecurityMiddleware.check(true, SecurityCheckType.TEACHING_STAFF), ParamsValidatorMiddleware.validate(this.assignmentValidator), this.createAssignment.bind(this));

        backend.patch('/assignments/:assignmentNameOrUrl/publish', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_STAFF), this.changeAssignmentPublishedStatus(true).bind(this));
        backend.patch('/assignments/:assignmentNameOrUrl/unpublish', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_STAFF), this.changeAssignmentPublishedStatus(false).bind(this));

        backend.post('/assignments/:assignmentNameOrUrl/corrections', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_STAFF), ParamsValidatorMiddleware.validate(this.assignmentAddCorrigeValidator), this.linkUpdateAssignmentCorrection(false).bind(this));
        backend.patch('/assignments/:assignmentNameOrUrl/corrections/:exerciseIdOrUrl', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_STAFF), this.linkUpdateAssignmentCorrection(true).bind(this));
    }

    // Get an assignment by its name or gitlab url
    private async getAssignment(req: express.Request, res: express.Response) {
        const assignment: Partial<Assignment> | undefined = req.boundParams.assignment;

        if ( assignment && !assignment.published && !await AssignmentManager.isUserAllowedToAccessAssignment(assignment as Assignment, req.session.profile) ) {
            delete assignment.gitlabId;
            delete assignment.gitlabLink;
            delete assignment.gitlabCreationInfo;
            delete assignment.gitlabLastInfo;
            delete assignment.gitlabLastInfoDate;
            delete assignment.staff;
            delete assignment.exercises;
        }

        return assignment ? req.session.sendResponse(res, StatusCodes.OK, DojoModelsHelper.getFullSerializableObject(assignment)) : res.status(StatusCodes.NOT_FOUND).send();
    }

    private async createAssignment(req: express.Request, res: express.Response) {
        const params: {
            name: string, members: Array<Gitlab.UserSchema>, template: string
        } = req.body;
        params.members = [ await req.session.profile.gitlabProfile.value, ...params.members ];
        params.members = params.members.removeObjectDuplicates(gitlabUser => gitlabUser.id);


        let repository: Gitlab.ProjectSchema;
        try {
            repository = await GitlabManager.createRepository(params.name, Config.assignment.default.description.replace('{{ASSIGNMENT_NAME}}', params.name), Config.assignment.default.visibility, Config.assignment.default.initReadme, Config.gitlab.group.assignments, Config.assignment.default.sharedRunnersEnabled, Config.assignment.default.wikiEnabled, params.template);
        } catch ( error ) {
            logger.error('Repo creation error');
            logger.error(JSON.stringify(error));

            if ( error instanceof GitbeakerRequestError ) {
                if ( error.cause?.description ) {
                    const description = error.cause.description as unknown;
                    if ( description instanceof Object && 'name' in description && description.name instanceof Array && description.name.length > 0 && description.name[0] === 'has already been taken' ) {
                        return req.session.sendResponse(res, StatusCodes.CONFLICT, {}, `Repository name has already been taken`, DojoStatusCode.ASSIGNMENT_NAME_CONFLICT);
                    }
                }

                return req.session.sendResponse(res, error.cause?.response.status ?? StatusCodes.INTERNAL_SERVER_ERROR);
            }

            return req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR);
        }

        await new Promise(resolve => setTimeout(resolve, Config.gitlab.repository.timeoutAfterCreation));

        try {
            await GitlabManager.protectBranch(repository.id, '*', true, Gitlab.AccessLevel.DEVELOPER, Gitlab.AccessLevel.DEVELOPER, Gitlab.AccessLevel.ADMIN);

            await GitlabManager.addRepositoryBadge(repository.id, Config.gitlab.badges.pipeline.link, Config.gitlab.badges.pipeline.imageUrl, 'Pipeline Status');
        } catch ( error ) {
            return GlobalHelper.repositoryCreationError('Repo params error', error, req, res, DojoStatusCode.ASSIGNMENT_CREATION_GITLAB_ERROR, DojoStatusCode.ASSIGNMENT_CREATION_INTERNAL_ERROR, repository);
        }

        try {
            await GitlabManager.deleteFile(repository.id, '.gitlab-ci.yml', 'Remove .gitlab-ci.yml');
        } catch ( error ) { /* empty */ }

        try {
            await GitlabManager.createFile(repository.id, '.gitlab-ci.yml', fs.readFileSync(path.join(__dirname, '../../assets/assignment_gitlab_ci.yml'), 'base64'), 'Add .gitlab-ci.yml (DO NOT MODIFY THIS FILE)');
        } catch ( error ) {
            return GlobalHelper.repositoryCreationError('CI file error', error, req, res, DojoStatusCode.ASSIGNMENT_CREATION_GITLAB_ERROR, DojoStatusCode.ASSIGNMENT_CREATION_INTERNAL_ERROR, repository);
        }

        try {
            await Promise.all(params.members.map(member => member.id).map(async (memberId: number): Promise<Gitlab.MemberSchema | false> => {
                try {
                    return await GitlabManager.addRepositoryMember(repository.id, memberId, Gitlab.AccessLevel.DEVELOPER);
                } catch ( error ) {
                    logger.error('Add member error');
                    logger.error(JSON.stringify(error));
                    return false;
                }
            }));

            const assignment: Assignment = await db.assignment.create({
                                                                          data: {
                                                                              name              : repository.name,
                                                                              gitlabId          : repository.id,
                                                                              gitlabLink        : repository.web_url,
                                                                              gitlabCreationInfo: repository as unknown as Prisma.JsonObject,
                                                                              gitlabLastInfo    : repository as unknown as Prisma.JsonObject,
                                                                              gitlabLastInfoDate: new Date(),
                                                                              staff             : {
                                                                                  connectOrCreate: [ ...params.members.map(gitlabUser => {
                                                                                      return {
                                                                                          create: {
                                                                                              id            : gitlabUser.id,
                                                                                              gitlabUsername: gitlabUser.name
                                                                                          },
                                                                                          where : {
                                                                                              id: gitlabUser.id
                                                                                          }
                                                                                      };
                                                                                  }) ]
                                                                              }
                                                                          }
                                                                      }) as unknown as Assignment;

            return req.session.sendResponse(res, StatusCodes.OK, assignment);
        } catch ( error ) {
            return GlobalHelper.repositoryCreationError('DB error', error, req, res, DojoStatusCode.ASSIGNMENT_CREATION_GITLAB_ERROR, DojoStatusCode.ASSIGNMENT_CREATION_INTERNAL_ERROR, repository);
        }
    }

    private changeAssignmentPublishedStatus(publish: boolean): (req: express.Request, res: express.Response) => Promise<void> {
        return async (req: express.Request, res: express.Response): Promise<void> => {
            if ( publish ) {
                const isPublishable = await SharedAssignmentHelper.isPublishable(req.boundParams.assignment!.gitlabId);
                if ( !isPublishable.isPublishable ) {
                    req.session.sendResponse(res, StatusCodes.BAD_REQUEST, { lastPipeline: isPublishable.lastPipeline }, isPublishable.status?.message, isPublishable.status?.code);
                    return;
                }
            }

            try {
                await GitlabManager.changeRepositoryVisibility(req.boundParams.assignment!.gitlabId, publish ? 'internal' : 'private');

                await db.assignment.update({
                                               where: {
                                                   name: req.boundParams.assignment!.name
                                               },
                                               data : {
                                                   published: publish
                                               }
                                           });

                req.session.sendResponse(res, StatusCodes.OK);
            } catch ( error ) {
                logger.error(JSON.stringify(error));

                if ( error instanceof GitbeakerRequestError ) {
                    req.session.sendResponse(res, error.cause?.response.status ?? StatusCodes.INTERNAL_SERVER_ERROR, undefined, 'Error while updating the assignment state');
                    return;
                }

                req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, undefined, 'Error while updating the assignment state');
            }
        };
    }

    private linkUpdateAssignmentCorrection(isUpdate: boolean): (req: express.Request, res: express.Response) => Promise<void> {
        return async (req: express.Request, res: express.Response): Promise<void> => {
            if ( req.boundParams.exercise?.assignmentName !== req.boundParams.assignment?.name ) {
                return req.session.sendResponse(res, StatusCodes.BAD_REQUEST, undefined, 'The exercise does not belong to the assignment', DojoStatusCode.ASSIGNMENT_EXERCISE_NOT_RELATED);
            }

            if ( !req.boundParams.assignment?.published ) {
                return req.session.sendResponse(res, StatusCodes.BAD_REQUEST, undefined, 'The assignment must be public', DojoStatusCode.ASSIGNMENT_NOT_PUBLISHED);
            }

            if ( !isUpdate && req.boundParams.exercise?.isCorrection ) {
                return req.session.sendResponse(res, StatusCodes.BAD_REQUEST, undefined, 'This exercise is already a correction', DojoStatusCode.EXERCISE_CORRECTION_ALREADY_EXIST);
            } else if ( isUpdate && !req.boundParams.exercise?.isCorrection ) {
                return req.session.sendResponse(res, StatusCodes.BAD_REQUEST, undefined, 'This exercise is not a correction', DojoStatusCode.EXERCISE_CORRECTION_NOT_EXIST);
            }

            const lastCommit = await GitlabManager.getRepositoryLastCommit(req.boundParams.exercise!.gitlabId);
            if ( lastCommit ) {
                if ( !isUpdate ) {
                    await GitlabManager.changeRepositoryVisibility(req.boundParams.exercise!.gitlabId, 'internal');
                }

                await db.exercise.update({
                                             where: {
                                                 id: req.boundParams.exercise!.id
                                             },
                                             data : {
                                                 correctionCommit: lastCommit
                                             }
                                         });

                return req.session.sendResponse(res, StatusCodes.OK);
            } else {
                return req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, undefined, 'No last commit found');
            }
        };
    }
}


export default new AssignmentRoutes();