import CommanderCommand                     from '../../CommanderCommand';
import Config                               from '../../../config/Config';
import fs                                   from 'node:fs';
import ora                                  from 'ora';
import util                                 from 'util';
import { exec }                             from 'child_process';
import chalk                                from 'chalk';
import * as os                              from 'os';
import path                                 from 'path';
import ClientsSharedConfig                  from '../../../sharedByClients/config/ClientsSharedConfig';
import AssignmentFile                       from '../../../shared/types/Dojo/AssignmentFile';
import ExerciseDockerCompose                from '../../../sharedByClients/helpers/Dojo/ExerciseDockerCompose';
import SharedAssignmentHelper               from '../../../shared/helpers/Dojo/SharedAssignmentHelper';
import ExerciseCheckerError                 from '../../../shared/types/Dojo/ExerciseCheckerError';
import ClientsSharedExerciseHelper          from '../../../sharedByClients/helpers/Dojo/ClientsSharedExerciseHelper';
import ExerciseResultsSanitizerAndValidator from '../../../sharedByClients/helpers/Dojo/ExerciseResultsSanitizerAndValidator';


const execAsync = util.promisify(exec);


class ExerciseRunCommand extends CommanderCommand {
    protected commandName: string = 'run';

    private readonly dateISOString: string = (new Date()).toISOString().replace(/:/g, '_').replace(/\./g, '_');

    private readonly folderResultsVolume: string = path.join(os.homedir(), 'DojoExecutions', `dojo_execLogs_${ this.dateISOString }`);
    private readonly folderResultsDojo: string = path.join(this.folderResultsVolume, `Dojo/`);
    private readonly folderResultsExercise: string = path.join(this.folderResultsVolume, `Exercise/`);

    private readonly projectName: string = `${ ClientsSharedConfig.dockerCompose.projectName }`;

    private readonly fileComposeLogs: string = path.join(this.folderResultsDojo, `dockerComposeLogs.txt`);

    protected defineCommand() {
        this.command
        .description('locally run an exercise')
        .option('-p, --path <value>', 'exercise path', Config.folders.defaultLocalExercise)
        .option('-v, --verbose', 'verbose mode (display docker compose logs in live)')
        .action(this.commandAction.bind(this));
    }

    private displayExecutionLogs() {
        ora({
                text  : `${ chalk.magenta('Execution logs folder:') } ${ this.folderResultsVolume }`,
                indent: 0
            }).start().info();
    }

    protected async commandAction(options: { path: string, verbose: boolean }): Promise<void> {
        const localExercisePath: string = options.path ?? Config.folders.defaultLocalExercise;

        let assignmentFile: AssignmentFile;
        let exerciseDockerCompose: ExerciseDockerCompose;
        let exerciseResultsValidation: ExerciseResultsSanitizerAndValidator;

        let haveResultsVolume: boolean;

        // Step 1: Check requirements (if it's an exercise folder and if Docker daemon is running)
        {
            console.log(chalk.cyan('Please wait while we are checking and creating dependencies...'));

            // Create result temp folder
            fs.mkdirSync(this.folderResultsVolume, { recursive: true });
            fs.mkdirSync(this.folderResultsDojo, { recursive: true });
            fs.mkdirSync(this.folderResultsExercise, { recursive: true });


            ora({
                    text  : `Checking exercise content:`,
                    indent: 4
                }).start().info();

            // Exercise folder
            {
                const spinner: ora.Ora = ora({
                                                 text  : `Checking exercise folder`,
                                                 indent: 8
                                             }).start();

                const files = fs.readdirSync(options.path);
                const missingFiles = Config.exercise.neededFiles.map((file: string): [ string, boolean ] => [ file, files.includes(file) ]).filter((file: [ string, boolean ]) => !file[1]);

                if ( missingFiles.length > 0 ) {
                    spinner.fail(`The exercise folder is missing the following files: ${ missingFiles.map((file: [ string, boolean ]) => file[0]).join(', ') }`);
                    return;
                }

                spinner.succeed(`The exercise folder contains all the needed files`);
            }

            // dojo_assignment.json validity
            {
                const spinner: ora.Ora = ora({
                                                 text  : `Checking ${ ClientsSharedConfig.assignment.filename } file`,
                                                 indent: 8
                                             }).start();

                const validationResults = SharedAssignmentHelper.validateDescriptionFile(path.join(options.path, ClientsSharedConfig.assignment.filename));
                if ( !validationResults.isValid ) {
                    spinner.fail(`The ${ ClientsSharedConfig.assignment.filename } file is invalid: ${ JSON.stringify(validationResults.errors) }`);
                    return;
                } else {
                    assignmentFile = validationResults.results!;
                }

                haveResultsVolume = assignmentFile.result.volume !== undefined;

                spinner.succeed(`The ${ ClientsSharedConfig.assignment.filename } file is valid`);
            }

            // Docker daemon
            {
                const spinner: ora.Ora = ora({
                                                 text  : `Checking Docker daemon`,
                                                 indent: 4
                                             }).start();

                try {
                    await execAsync(`docker ps`);
                } catch ( error ) {
                    spinner.fail(`The Docker daemon is not running`);
                    return;
                }

                spinner.succeed(`The Docker daemon is running`);
            }
        }


        // Step 2: Run docker-compose file
        {
            console.log(chalk.cyan('Please wait while we are running the exercise...'));

            let composeFileOverride: string[] = [];
            const composeOverridePath: string = path.join(localExercisePath, 'docker-compose-override.yml');
            if ( haveResultsVolume ) {
                const composeOverride = fs.readFileSync(path.join(__dirname, '../../../../assets/docker-compose-override.yml'), 'utf8').replace('{{VOLUME_NAME}}', assignmentFile.result.volume!).replace('{{MOUNT_PATH}}', this.folderResultsExercise);
                fs.writeFileSync(composeOverridePath, composeOverride);

                composeFileOverride = [ composeOverridePath ];
            }

            exerciseDockerCompose = new ExerciseDockerCompose(this.projectName, assignmentFile, localExercisePath, composeFileOverride);

            try {
                await new Promise<void>((resolve, reject) => {
                    let spinner: ora.Ora;

                    if ( options.verbose ) {
                        exerciseDockerCompose.events.on('logs', (log: string, _error: boolean, displayable: boolean) => {
                            if ( displayable ) {
                                console.log(log);
                            }
                        });
                    }

                    exerciseDockerCompose.events.on('step', (name: string, message: string) => {
                        spinner = ora({
                                          text  : message,
                                          indent: 4
                                      }).start();

                        if ( options.verbose && name == 'COMPOSE_RUN' ) {
                            spinner.info();
                        }
                    });

                    exerciseDockerCompose.events.on('endStep', (stepName: string, message: string, error: boolean) => {
                        if ( error ) {
                            if ( options.verbose && stepName == 'COMPOSE_RUN' ) {
                                ora({
                                        text  : message,
                                        indent: 4
                                    }).start().fail();
                            } else {
                                spinner.fail(message);
                            }
                        } else {
                            if ( options.verbose && stepName == 'COMPOSE_RUN' ) {
                                ora({
                                        text  : message,
                                        indent: 4
                                    }).start().succeed();
                            } else {
                                spinner.succeed(message);
                            }
                        }
                    });

                    exerciseDockerCompose.events.on('finished', (success: boolean) => {
                        success ? resolve() : reject();
                    });

                    exerciseDockerCompose.run(true);
                });
            } catch ( error ) { /* empty */ }

            fs.rmSync(composeOverridePath, { force: true });
            fs.writeFileSync(this.fileComposeLogs, exerciseDockerCompose.allLogs);

            if ( !exerciseDockerCompose.success ) {
                this.displayExecutionLogs();
                return;
            }
        }


        // Step 3: Get results
        {
            console.log(chalk.cyan('Please wait while we are checking the results...'));

            exerciseResultsValidation = new ExerciseResultsSanitizerAndValidator(this.folderResultsDojo, this.folderResultsExercise, exerciseDockerCompose.exitCode);

            try {
                await new Promise<void>((resolve, reject) => {
                    let spinner: ora.Ora;

                    exerciseResultsValidation.events.on('step', (name: string, message: string) => {
                        spinner = ora({
                                          text  : message,
                                          indent: 4
                                      }).start();
                    });

                    exerciseResultsValidation.events.on('endStep', (stepName: string, message: string, error: boolean) => {
                        if ( error ) {
                            if ( stepName == 'CHECK_SIZE' ) {
                                spinner.warn(message);
                            } else {
                                spinner.fail(message);
                            }
                        } else {
                            spinner.succeed(message);
                        }
                    });

                    exerciseResultsValidation.events.on('finished', (success: boolean, exitCode: number) => {
                        success || exitCode == ExerciseCheckerError.EXERCISE_RESULTS_FOLDER_TOO_BIG ? resolve() : reject();
                    });

                    exerciseResultsValidation.run();
                });
            } catch ( error ) {
                this.displayExecutionLogs();
                return;
            }
        }


        // Step 4: Display results + Volume location
        {
            ClientsSharedExerciseHelper.displayExecutionResults(exerciseResultsValidation.exerciseResults!, exerciseDockerCompose.exitCode, {
                INFO   : chalk.bold,
                SUCCESS: chalk.green,
                FAILURE: chalk.red
            }, `\n\n${ chalk.bold('Execution results folder') } : ${ this.folderResultsVolume }`);
        }
    }
}


export default new ExerciseRunCommand();