import AssignmentFile                     from '../../../shared/types/Dojo/AssignmentFile';
import { TypedEmitter }                   from 'tiny-typed-emitter';
import ExerciseRunningEvents              from '../../types/Dojo/ExerciseRunningEvents';
import { spawn }                          from 'child_process';
import ExerciseCheckerError               from '../../../shared/types/Dojo/ExerciseCheckerError';
import { ChildProcessWithoutNullStreams } from 'node:child_process';


class ExerciseDockerCompose {
    readonly events: TypedEmitter<ExerciseRunningEvents> = new TypedEmitter<ExerciseRunningEvents>();

    public displayableLogs: string = '';
    public allLogs: string = '';

    public isFinished: boolean = false;
    public success: boolean = false;
    public exitCode: number = -1;

    constructor(private projectName: string, private assignmentFile: AssignmentFile, private executionFolder: string, private composeFileOverride: Array<string> = []) {
        this.events.on('logs', (log: string, _error: boolean, displayable: boolean) => {
            this.allLogs += log;
            this.displayableLogs += displayable ? log : '';
        });

        this.events.on('finished', (success: boolean, exitCode: number) => {
            this.isFinished = true;
            this.success = success;
            this.exitCode = exitCode;
        });
    }

    private registerChildProcess(childProcess: ChildProcessWithoutNullStreams, resolve: (value: (number | PromiseLike<number>)) => void, reject: (reason?: unknown) => void) {
        childProcess.stdout.on('data', (data) => {
            this.events.emit('logs', data.toString(), false, false);
        });

        childProcess.stderr.on('data', (data) => {
            this.events.emit('logs', data.toString(), true, false);
        });

        childProcess.on('exit', (code) => {
            code !== null ? resolve(code) : reject();
        });
    }

    run(doDown: boolean = false) {
        (async () => {
            let containerExitCode: number = -1;

            const dockerComposeCommand = `docker compose --project-name ${ this.projectName } --progress plain --file docker-compose.yml ${ this.composeFileOverride.map((file) => `--file "${ file }"`).join(' ') }`;

            // Run the service
            {
                try {
                    this.events.emit('step', 'COMPOSE_RUN', 'Running Docker Compose file');

                    containerExitCode = await new Promise<number>((resolve, reject) => {

                        this.events.emit('logs', '####################################################### Docker Compose & Main Container Logs #######################################################\n', false, false);

                        const dockerCompose = spawn(`${ dockerComposeCommand } run --build --rm ${ this.assignmentFile.result.container }`, {
                            cwd  : this.executionFolder,
                            shell: true,
                            env  : {
                                'DOCKER_BUILDKIT'  : '1',
                                'BUILDKIT_PROGRESS': 'plain', ...process.env
                            }
                        });

                        this.registerChildProcess(dockerCompose, resolve, reject);
                    });
                } catch ( error ) {
                    this.events.emit('endStep', 'COMPOSE_RUN', `Error while running the docker compose file`, true);
                    this.events.emit('finished', false, ExerciseCheckerError.DOCKER_COMPOSE_RUN_ERROR);
                    return;
                }
                this.events.emit('endStep', 'COMPOSE_RUN', `Docker Compose file run successfully`, false);
            }

            // Get linked services logs
            {
                try {
                    this.events.emit('step', 'COMPOSE_LOGS', 'Linked services logs acquisition');

                    await new Promise<number>((resolve, reject) => {

                        this.events.emit('logs', '####################################################### Other Services Logs #######################################################\n', false, false);

                        const dockerCompose = spawn(`${ dockerComposeCommand } logs --timestamps`, {
                            cwd  : this.executionFolder,
                            shell: true
                        });

                        this.registerChildProcess(dockerCompose, resolve, reject);
                    });
                } catch ( error ) {
                    this.events.emit('endStep', 'COMPOSE_LOGS', `Error while getting the linked services logs`, true);
                    this.events.emit('finished', false, ExerciseCheckerError.DOCKER_COMPOSE_LOGS_ERROR);
                    return;
                }
                this.events.emit('endStep', 'COMPOSE_LOGS', `Linked services logs acquired`, false);
            }

            // Remove containers if asked
            {
                if ( doDown ) {
                    try {
                        this.events.emit('step', 'COMPOSE_DOWN', 'Stopping and removing containers');

                        await new Promise<number>((resolve, reject) => {

                            this.events.emit('logs', '####################################################### Stop and remove containers #######################################################\n', false, false);

                            const dockerCompose = spawn(`${ dockerComposeCommand } down --volumes --rmi`, {
                                cwd  : this.executionFolder,
                                shell: true
                            });

                            this.registerChildProcess(dockerCompose, resolve, reject);
                        });
                    } catch ( error ) {
                        this.events.emit('endStep', 'COMPOSE_DOWN', `Error stop and remove containers`, true);
                        this.events.emit('finished', false, ExerciseCheckerError.DOCKER_COMPOSE_DOWN_ERROR);
                        return;
                    }
                    this.events.emit('endStep', 'COMPOSE_DOWN', `Containers stopped and removed`, false);
                }
            }

            // Remove images if asked
            {
                if ( doDown ) {
                    try {
                        this.events.emit('step', 'COMPOSE_REMOVE_DANGLING', 'Removing dangling images');

                        await new Promise<number>((resolve, reject) => {

                            this.events.emit('logs', '####################################################### Remove dangling images #######################################################\n', false, false);

                            const dockerCompose = spawn(`docker image prune --force`, {
                                cwd  : this.executionFolder,
                                shell: true
                            });

                            this.registerChildProcess(dockerCompose, resolve, reject);
                        });
                    } catch ( error ) {
                        this.events.emit('endStep', 'COMPOSE_REMOVE_DANGLING', `Error while removing dangling images`, true);
                        this.events.emit('finished', false, ExerciseCheckerError.DOCKER_COMPOSE_REMOVE_DANGLING_ERROR);
                        return;
                    }
                    this.events.emit('endStep', 'COMPOSE_REMOVE_DANGLING', `Dangling images removed`, false);
                }
            }

            this.events.emit('finished', true, containerExitCode);
        })();
    }
}


export default ExerciseDockerCompose;