import { Express }                 from 'express-serve-static-core';
import express, { RequestHandler } from 'express';
import * as ExpressValidator       from 'express-validator';
import { StatusCodes }             from 'http-status-codes';
import RoutesManager               from '../express/RoutesManager.js';
import ParamsValidatorMiddleware   from '../middlewares/ParamsValidatorMiddleware.js';
import SecurityMiddleware          from '../middlewares/SecurityMiddleware.js';
import GitlabManager               from '../managers/GitlabManager.js';
import Config                      from '../config/Config.js';
import logger                      from '../shared/logging/WinstonLogger.js';
import DojoValidators              from '../helpers/DojoValidators.js';
import { v4 as uuidv4 }            from 'uuid';
import { Prisma }                  from '@prisma/client';
import { Assignment, Exercise }    from '../types/DatabaseTypes.js';
import db                          from '../helpers/DatabaseHelper.js';
import SecurityCheckType           from '../types/SecurityCheckType.js';
import JSON5                       from 'json5';
import fs                          from 'fs';
import path                        from 'path';
import AssignmentFile              from '../shared/types/Dojo/AssignmentFile.js';
import ExerciseResultsFile         from '../shared/types/Dojo/ExerciseResultsFile.js';
import DojoStatusCode              from '../shared/types/Dojo/DojoStatusCode.js';
import GlobalHelper                from '../helpers/GlobalHelper.js';
import { IFileDirStat }            from '../shared/helpers/recursiveFilesStats/RecursiveFilesStats.js';
import ExerciseManager             from '../managers/ExerciseManager.js';
import * as Gitlab                 from '@gitbeaker/rest';
import GitlabTreeFileType          from '../shared/types/Gitlab/GitlabTreeFileType.js';
import { GitbeakerRequestError }   from '@gitbeaker/requester-utils';


class ExerciseRoutes implements RoutesManager {
    private readonly exerciseValidator: ExpressValidator.Schema = {
        members: {
            trim           : true,
            notEmpty       : true,
            customSanitizer: DojoValidators.jsonSanitizer
        }
    };

    private readonly resultValidator: ExpressValidator.Schema = {
        exitCode     : {
            isInt: true,
            toInt: true
        },
        commit       : {
            trim           : true,
            notEmpty       : true,
            customSanitizer: DojoValidators.jsonSanitizer
        },
        results      : {
            trim           : true,
            notEmpty       : true,
            custom         : DojoValidators.exerciseResultsValidator,
            customSanitizer: DojoValidators.jsonSanitizer
        },
        files        : {
            trim           : true,
            notEmpty       : true,
            customSanitizer: DojoValidators.jsonSanitizer
        },
        archiveBase64: {
            isBase64: true,
            notEmpty: true
        }
    };

    registerOnBackend(backend: Express) {
        backend.post('/assignments/:assignmentNameOrUrl/exercises', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_IS_PUBLISHED), ParamsValidatorMiddleware.validate(this.exerciseValidator), this.createExercise.bind(this) as RequestHandler);

        backend.get('/exercises', SecurityMiddleware.check(true, SecurityCheckType.ADMIN), this.getAllExercises.bind(this) as RequestHandler);
        backend.get('/exercises/:exerciseIdOrUrl/assignment', SecurityMiddleware.check(false, SecurityCheckType.EXERCISE_SECRET), this.getAssignment.bind(this) as RequestHandler);
        backend.get('/exercises/:exerciseIdOrUrl/members', SecurityMiddleware.check(true, SecurityCheckType.ADMIN, SecurityCheckType.EXERCISE_MEMBERS), this.getExerciseMembers.bind(this) as RequestHandler);
        backend.get('/exercises/:exerciseIdOrUrl/results', SecurityMiddleware.check(true, SecurityCheckType.ADMIN, SecurityCheckType.EXERCISE_MEMBERS), this.getExerciseResults.bind(this) as RequestHandler);

        backend.delete('/exercises/:exerciseIdOrUrl', SecurityMiddleware.check(true, SecurityCheckType.ADMIN, SecurityCheckType.EXERCISE_MEMBERS), this.deleteExercise.bind(this) as RequestHandler);

        backend.post('/exercises/:exerciseIdOrUrl/results', SecurityMiddleware.check(false, SecurityCheckType.EXERCISE_SECRET), ParamsValidatorMiddleware.validate(this.resultValidator), this.createResult.bind(this) as RequestHandler);
    }

    private getExerciseName(assignment: Assignment, members: Array<Gitlab.UserSchema>, suffix: number): string {
        const memberNames: string = members.map(member => member.username).sort((a, b) => a.localeCompare(b)).join(' + ');
        const suffixString: string = suffix > 0 ? ` - ${ suffix }` : '';
        return `DojoEx - ${ assignment.name } - ${ memberNames }${ suffixString }`;
    }

    private async getExerciseMembers(req: express.Request, res: express.Response) {
        const repoId = req.boundParams.exercise!.gitlabId;

        const members = await GitlabManager.getRepositoryMembers(String(repoId));

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

    private async getExerciseResults(req: express.Request, res: express.Response) {
        const results = await db.result.findMany({
                                                     where  : { exerciseId: req.boundParams.exercise!.id },
                                                     orderBy: { dateTime: 'desc' }
                                                 });

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

    private async deleteExercise(req: express.Request, res: express.Response) {
        const repoId = req.boundParams.exercise!.gitlabId;

        const members = await GitlabManager.getRepositoryMembers(String(repoId), false);
        for ( const member of members ) {
            if ( member.id !== Config.gitlab.account.id ) {
                await GitlabManager.deleteRepositoryMember(repoId, member.id);
            }
        }

        await GitlabManager.moveRepository(repoId, Config.gitlab.group.deletedExercises);

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

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

    private getExercisePath(assignment: Assignment, exerciseId: string): string {
        return `dojo-ex_${ (assignment.gitlabLastInfo as unknown as Gitlab.ProjectSchema).path }_${ exerciseId }`;
    }

    // Get all exercise
    private async getAllExercises(req: express.Request, res: express.Response) {
        const exos = await db.exercise.findMany();

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


    private async checkExerciseLimit(assignment: Assignment, members: Array<Gitlab.UserSchema>): Promise<Array<Gitlab.UserSchema>> {
        const exercises: Array<Exercise> | undefined = await ExerciseManager.getFromAssignment(assignment.name, { members: true });
        const reachedLimitUsers: Array<Gitlab.UserSchema> = [];
        if ( exercises.length > 0 ) {
            for ( const member of members ) {
                const exerciseCount: number = exercises.filter(exercise => exercise.members.findIndex(exerciseMember => exerciseMember.id === member.id) !== -1).length;
                if ( exerciseCount >= Config.exercise.maxPerAssignment ) {
                    reachedLimitUsers.push(member);
                }
            }
        }

        return reachedLimitUsers;
    }

    private async createExerciseRepository(assignment: Assignment, members: Array<Gitlab.UserSchema>, exerciseId: string, req: express.Request, res: express.Response): Promise<Gitlab.ProjectSchema | undefined> {
        let repository!: Gitlab.ProjectSchema;

        let suffix: number = 0;
        do {
            try {
                repository = await GitlabManager.forkRepository((assignment.gitlabCreationInfo as unknown as Gitlab.ProjectSchema).id, this.getExerciseName(assignment, members, suffix), this.getExercisePath(req.boundParams.assignment!, exerciseId), Config.exercise.default.description.replace('{{ASSIGNMENT_NAME}}', assignment.name), Config.exercise.default.visibility, Config.gitlab.group.exercises);
                break;
            } catch ( error ) {
                logger.error('Repo creation error');
                logger.error(JSON.stringify(error));

                if ( error instanceof GitbeakerRequestError && error.cause?.description ) {
                    const description = error.cause.description as unknown;
                    if ( GlobalHelper.isRepoNameAlreadyTaken(description) ) {
                        suffix++;
                    } else {
                        req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, {}, 'Unknown gitlab error while forking repository', DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR);
                        return undefined;
                    }
                } else {
                    req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, {}, 'Unknown error while forking repository', DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR);
                    return undefined;
                }
            }
        } while ( suffix < Config.exercise.maxSameName );

        return repository;
    }

    private async createExercise(req: express.Request, res: express.Response) {
        const params: { members: Array<Gitlab.UserSchema> } = req.body;
        params.members = [ await req.session.profile.gitlabProfile.value, ...params.members ].removeObjectDuplicates(gitlabUser => gitlabUser.id);
        const assignment: Assignment = req.boundParams.assignment!;


        const reachedLimitUsers: Array<Gitlab.UserSchema> = await this.checkExerciseLimit(assignment, params.members);
        if ( reachedLimitUsers.length > 0 ) {
            req.session.sendResponse(res, StatusCodes.INSUFFICIENT_SPACE_ON_RESOURCE, reachedLimitUsers, 'Max exercise per assignment reached', DojoStatusCode.MAX_EXERCISE_PER_ASSIGNMENT_REACHED);
            return;
        }


        const exerciseId: string = uuidv4();
        const secret: string = uuidv4();
        const repository: Gitlab.ProjectSchema | undefined = await this.createExerciseRepository(assignment, params.members, exerciseId, req, res);

        if ( !repository ) {
            return;
        }

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

        const repoCreationFnExec = GlobalHelper.repoCreationFnExecCreator(req, res, DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR, DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR, repository);

        try {
            await repoCreationFnExec(() => GitlabManager.protectBranch(repository.id, '*', false, Gitlab.AccessLevel.DEVELOPER, Gitlab.AccessLevel.DEVELOPER, Gitlab.AccessLevel.ADMIN), 'Branch protection modification error');
            await repoCreationFnExec(() => GitlabManager.addRepositoryBadge(repository.id, Config.gitlab.badges.pipeline.link, Config.gitlab.badges.pipeline.imageUrl, 'Pipeline Status'), 'Pipeline badge addition error');

            await repoCreationFnExec(async () => {
                await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_EXERCISE_ID', exerciseId, false, true);
                await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_SECRET', secret, false, true);
                await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_RESULTS_FOLDER', Config.exercise.pipelineResultsFolder, false, false);
            }, 'Pipeline variables addition error');

            await repoCreationFnExec(() => GitlabManager.updateFile(repository.id, '.gitlab-ci.yml', fs.readFileSync(path.join(__dirname, '../../assets/exercise_gitlab_ci.yml'), 'base64'), 'Add .gitlab-ci.yml (DO NOT MODIFY THIS FILE)'), 'CI/CD file update error');

            await repoCreationFnExec(async () => Promise.all([ ...new Set([ ...assignment.staff, ...params.members ].map(member => member.id)) ].map(GlobalHelper.addRepoMember(repository.id))), 'Add repository members error');

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

            req.session.sendResponse(res, StatusCodes.OK, exercise);
            return;
        } catch ( error ) {
            /* Empty */
        }
    }

    private async getAssignment(req: express.Request, res: express.Response) {
        const repoTree: Array<Gitlab.RepositoryTreeSchema> = await GitlabManager.getRepositoryTree(req.boundParams.exercise!.assignment.gitlabId);

        let assignmentHjsonFile!: Gitlab.RepositoryFileExpandedSchema;
        const immutableFiles: Array<Gitlab.RepositoryFileExpandedSchema> = await Promise.all(Config.assignment.baseFiles.map(async (baseFile: string) => {
            const file = await GitlabManager.getFile(req.boundParams.exercise!.assignment.gitlabId, baseFile);

            if ( baseFile === Config.assignment.filename ) {
                assignmentHjsonFile = file;
            }

            return file;
        }));

        const dojoAssignmentFile: AssignmentFile = JSON5.parse(atob(assignmentHjsonFile.content));

        const immutablePaths = dojoAssignmentFile.immutable.map(fileDescriptor => fileDescriptor.path);

        await Promise.all(repoTree.map(async gitlabTreeFile => {
            if ( gitlabTreeFile.type === GitlabTreeFileType.BLOB.valueOf() ) {
                for ( const immutablePath of immutablePaths ) {
                    if ( gitlabTreeFile.path.startsWith(immutablePath) ) {
                        immutableFiles.push(await GitlabManager.getFile(req.boundParams.exercise!.assignment.gitlabId, gitlabTreeFile.path));
                        break;
                    }
                }
            }
        }));

        return req.session.sendResponse(res, StatusCodes.OK, {
            assignment    : (req.boundParams.exercise as Exercise).assignment,
            assignmentFile: dojoAssignmentFile,
            immutable     : immutableFiles
        });
    }

    private async createResult(req: express.Request, res: express.Response) {
        const params: { exitCode: number, commit: Record<string, string>, results: ExerciseResultsFile, files: Array<IFileDirStat>, archiveBase64: string } = req.body;
        const exercise: Exercise = req.boundParams.exercise!;

        const result = await db.result.create({
                                                  data: {
                                                      exerciseId: exercise.id,
                                                      exitCode  : params.exitCode,
                                                      success   : params.results.success!,
                                                      commit    : params.commit,
                                                      results   : params.results as unknown as Prisma.JsonObject,
                                                      files     : params.files
                                                  }
                                              });

        fs.writeFileSync(path.join(Config.getResultsFolder(exercise), `${ result.dateTime.toISOString().replace(/:/g, '_') }.tar.gz`), params.archiveBase64, 'base64');

        req.session.sendResponse(res, StatusCodes.OK);
    }
}


export default new ExerciseRoutes();