package de.hftstuttgart.dtabackend.utils;

import com.fasterxml.jackson.core.exc.StreamReadException;
import com.fasterxml.jackson.databind.DatabindException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.dockerjava.api.model.Bind;
import com.github.dockerjava.api.model.Volume;

import de.hftstuttgart.dtabackend.models.ExerciseCompetencyProfile;
import de.hftstuttgart.dtabackend.models.Recommendation;
import de.hftstuttgart.dtabackend.models.ResultSummary;
import de.hftstuttgart.dtabackend.models.TestCompetencyProfile;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

import java.io.*;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Component
public class ExecuteTestUtil {
    private static final Logger LOG = LogManager.getLogger(ExecuteTestUtil.class);

    private final DockerUtil dockerUtil;
    private final String basePathProfessorUnitTests;
    private final String basePathStudentsSubmissionCode;

    public ExecuteTestUtil(Environment env, DockerUtil dockerUtil) {
        this.dockerUtil = dockerUtil;

        // set base path for assignments to be stored
        Path p = Paths.get(
            env.getProperty("data.dir"), ///data
            env.getProperty("data.dir.test.folder.name")); //UnitTests
        this.basePathProfessorUnitTests = p.toAbsolutePath().toString();
        this.basePathStudentsSubmissionCode = env.getProperty( "tests.tmp.dir");// /dta-test-assignments
    }

    public ResultSummary runTests(String assignmentId, Path workDirectory) throws IOException, InterruptedException {
        // Define paths for the submission-specific directories
        String submissionDirectory = this.basePathStudentsSubmissionCode + workDirectory.getFileName(); // /dta-test-assignments/dta-submissionID
        Path testPath = Paths.get(submissionDirectory, "test");
        Path srcPath = Paths.get(submissionDirectory, "src");
        Path resultPath = Paths.get(submissionDirectory, "result");

        // Ensure directories exist
        Files.createDirectories(testPath);
        Files.createDirectories(srcPath);
        Files.createDirectories(resultPath);

        // Clone stored test to testPath
        LOG.debug("Copying pre-downloaded unit test repo");
        FileUtil.copyFolder(Paths.get(basePathProfessorUnitTests, assignmentId), testPath);

        LOG.debug("Copying exercise manifest");
        Files.copy(
            Paths.get(basePathProfessorUnitTests, assignmentId + "_checkout", CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME),
            testPath.resolve(CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME)
        );

        LOG.debug("Copying test config");
        Files.copy(
            Paths.get(basePathProfessorUnitTests, assignmentId + ".txt"),
            Paths.get(submissionDirectory, "config.txt")
        );

        LOG.info("Reading test config");
        Matcher config = RegexUtil.extractConfig(
            new FileInputStream(Paths.get(submissionDirectory, "config.txt").toFile()),
            Pattern.compile(RegexUtil.DTA_TESTCONFIGREGEX)
        );
        String image;
        if (config == null) {
            config = RegexUtil.extractConfig(
                new FileInputStream(Paths.get(submissionDirectory, "config.txt").toFile()),
                Pattern.compile(RegexUtil.TESTCONFIGREGEX)
            );
            if (config == null) {
                throw new RuntimeException("Couldn't find repo config for unit test image extraction");
            }
            image = config.group(4);
        } else {
            image = config.group(5);
        }

        // Start the test-container with professor-given image and submission-specific volume mounts
        dockerUtil.runContainerWithBinds(
            image,
            new Bind(testPath.toString(), new Volume("/data/test")),
            new Bind(srcPath.toString(), new Volume("/data/src")),
            new Bind(resultPath.toString(), new Volume("/data/result"))
        );

        return generateResult(assignmentId, resultPath, testPath);
    }

    private ResultSummary generateResult(String assignmentId, Path resultPath, Path testPath)
            throws IOException, StreamReadException, DatabindException, MalformedURLException {
        // Define expected result file
        File resultFile = resultPath.resolve("result.json").toFile();

        // Check if result file is there
        if (!resultFile.exists() || !resultFile.isFile()) {
            LOG.error("Could not find result file in {}", resultFile.getAbsolutePath());
            throw new RuntimeException("No result file found");
        }

        LOG.debug("Parsing results JSON");
        ObjectMapper objectMapper = new ObjectMapper();
        ResultSummary resultSummary = objectMapper.readValue(resultFile.toURI().toURL(), ResultSummary.class);
        LOG.debug("Result JSON returned time {} with {} test results.", resultSummary.timestamp, resultSummary.results.size());

        LOG.info("Checking for optional test competency profile information for pedagogical agent functionality...");
        List<TestCompetencyProfile> testCompetencyProfiles = CompetencyAssessmentUtil.readTestCompetencyProfiles(testPath, CompetencyAssessmentUtil.TEST_COMPETENCY_MANIFEST_FILE_NAME);
        LOG.debug(String.format(
            "Reading Test Competency Profiles: basePath=%s, fileName=%s",
            testPath,
            CompetencyAssessmentUtil.TEST_COMPETENCY_MANIFEST_FILE_NAME
        ));
        if (testCompetencyProfiles != null) {
            LOG.info("Found optional test competency profiles, generating agent profile data...");
            resultSummary.overallTestCompetencyProfile = CompetencyAssessmentUtil.packFloats(CompetencyAssessmentUtil.sumTestCompetencyProfiles(testCompetencyProfiles));
            resultSummary.successfulTestCompetencyProfile = CompetencyAssessmentUtil.packFloats(CompetencyAssessmentUtil.sumSuccessfulCompetencyProfiles(testCompetencyProfiles, resultSummary, true));
            
            LOG.info("Checking for optional exercise competency profile information for pedagogical agent exercise recommendation functionality...");
            Path exerciseManifestFile = Paths.get(testPath.toString(), CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME);
            LOG.debug(String.format(
                "Constructing Path for exercise manifest: testPath=%s, fileName=%s -> Resulting Path=%s",
                testPath.toString(),
                CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME,
                exerciseManifestFile.toString()
            ));
            if (Files.exists(exerciseManifestFile)) {
                LOG.info("Found optional exercise competency profiles, generating recommendations...");
                resultSummary.recommendations = recommendNextExercises(assignmentId, testPath, testCompetencyProfiles, resultSummary);
            }
        }
        return resultSummary;
    }

	/*
	 * exercise recommendation part
	 */
	public List<Recommendation> recommendNextExercises(String assignmentId, Path testPath, List<TestCompetencyProfile> testCompetencyProfiles, ResultSummary resultSummary)
            throws FileNotFoundException {
		// fetch repo url from original test upload
        Pattern pattern = Pattern.compile(RegexUtil.DTA_TESTCONFIGREGEX);
        File configFile = Paths.get(basePathProfessorUnitTests, assignmentId + ".txt").toFile();
        Matcher config = RegexUtil.extractConfig(new FileInputStream(configFile), pattern);
        String testRepoURL = config.group(1);

        List<ExerciseCompetencyProfile> exerciseCompetencyProfiles = CompetencyAssessmentUtil.readExerciseCompetencyProfiles(
            testPath, CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME
        );

		int currentTopicIndex = 0;
		float currentDifficulty = 0.0f;

		//build course topic order
        Map<String, Integer> topicOrder = new HashMap<>();
        int order = 1;

        for (ExerciseCompetencyProfile e : exerciseCompetencyProfiles) {
            if (!topicOrder.containsKey(e.exerciseTopicName)) {
                topicOrder.put(e.exerciseTopicName, order++);
            }
            if (e.exerciseURL.equals(testRepoURL)) {
                currentTopicIndex = order;
                currentDifficulty = e.difficulty;
            }
        }

		//filter exercises according to success
        float[] unsuccessful = CompetencyAssessmentUtil.sumSuccessfulCompetencyProfiles(testCompetencyProfiles, resultSummary, false);
        List<ExerciseCompetencyProfile> filteredExercises = filterExercisesByTopicsAndDifficulty(
				exerciseCompetencyProfiles, topicOrder, currentTopicIndex, testRepoURL,
				currentDifficulty, unsuccessful, resultSummary);

		//compute recommendations
		List<Recommendation> recommendedExercises = new ArrayList<>();
        for (ExerciseCompetencyProfile exerciseProfile : filteredExercises) {
			Recommendation recommendation = new Recommendation(exerciseProfile.exerciseTopicName, exerciseProfile.exerciseURL, exerciseProfile.exerciseName,
					exerciseProfile.difficulty, calculateScore(exerciseProfile, unsuccessful, topicOrder, currentDifficulty));
			recommendedExercises.add(recommendation);
			LOG.info("Recommending exercise "+recommendation.topic+"/"+recommendation.exerciseName+" with score "+recommendation.score);
		}
		//sort the recommendations for successful or resilient learners, otherwise reverse in display
		recommendedExercises.stream().sorted(Recommendation.COMPARE_BY_SCORE).collect(Collectors.toList());
		
		return recommendedExercises;
    }

	public static List<ExerciseCompetencyProfile> filterExercisesByTopicsAndDifficulty(List<ExerciseCompetencyProfile> exerciseCompetencyProfiles,
			Map<String, Integer> topicOrder, int currentTopicIndex, String testRepoURL, float currentDifficulty, float[] unsuccessful, 
			ResultSummary resultSummary) {
		//filter out all advanced topics in any case
		//option for later: include next topic if fullsuccess and current difficulty == max difficulty
		List<ExerciseCompetencyProfile> filteredExercises = exerciseCompetencyProfiles.stream()
		        .filter(testProfile -> topicOrder.get(testProfile.exerciseTopicName) <= currentTopicIndex)
            .collect(Collectors.toList());

		//filter by difficulty according to success
        if (isFullSuccess(unsuccessful)) {
			filteredExercises = filteredExercises.stream().filter(profile -> profile.difficulty >= currentDifficulty && !testRepoURL.equals(profile.exerciseURL)).collect(Collectors.toList());
        } else {
			filteredExercises = filteredExercises.stream().filter(profile -> profile.difficulty <= currentDifficulty).collect(Collectors.toList());
        }

		return filteredExercises;
    }

    public static boolean isFullSuccess(float[] unsuccessful) {
        for (float value : unsuccessful) {
            if (value != 0.0f) {
                return false;
            }
        }
        return true;
    }

	public static float calculateScore(ExerciseCompetencyProfile exerciseProfile, float[] unsuccessful, Map<String, Integer> topicOrder, float currentDifficulty) {
		//ensure factor 1 for full success not to blank out score, thus offset the base
        float score = 1.0f;
		//competency profile difference to not fully achieved competencies component
		for (int i = 0; i < exerciseProfile.competencyAssessments.length-1; i++) {
			score += exerciseProfile.competencyAssessments[i] * unsuccessful[i];
        }
		
		//difficulty component
		score = score * (exerciseProfile.difficulty*(0.5f+Math.abs(currentDifficulty-exerciseProfile.difficulty)));
		
		//topic component
		score *= topicOrder.get(exerciseProfile.exerciseTopicName);

		score = Math.round(score * 10.0f) / 10.0f;
		return score;
    }
}