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.*;
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 assignmentBasePath;
    private final Path testTmpPathHost;

    public ExecuteTestUtil(Environment env, DockerUtil dockerUtil) {
        this.dockerUtil = dockerUtil;
        this.assignmentBasePath = Paths.get(
            env.getProperty("data.dir"),
            env.getProperty("data.dir.test.folder.name"))
            .toAbsolutePath().toString();

        this.testTmpPathHost = Paths.get(env.getProperty("host.tests.tmp.dir"));
    }

    public ResultSummary runTests(String assignmentId, Path workDirectory) throws IOException, InterruptedException {
        // Define paths
        Path testPath = workDirectory.resolve("test");
        Path srcPath = workDirectory.resolve("src");
        Path resultPath = workDirectory.resolve("result");

        // Clone test to temporary directory
        LOG.debug("Copying pre-downloaded unittest repo");
        FileUtil.copyFolder(Paths.get(assignmentBasePath, assignmentId), testPath);

        // Copy configuration file
        LOG.debug("Copy test config");
        Files.copy(Paths.get(assignmentBasePath, assignmentId + ".txt"), workDirectory.resolve("config.txt"));
        Files.createDirectory(resultPath);

        // Load container image configuration
        LOG.info("Reading test config");
        String image = loadImageConfig(workDirectory);

        // Define paths to mount in the container
        Path testPathHost = testTmpPathHost.resolve(workDirectory.getFileName()).resolve(testPath.getFileName());
        Path srcPathHost = testTmpPathHost.resolve(workDirectory.getFileName()).resolve(srcPath.getFileName());
        Path resultPathHost = testTmpPathHost.resolve(workDirectory.getFileName()).resolve(resultPath.getFileName());

        // Start container with mounts
        dockerUtil.runContainer(
            image,
            new Bind(testPathHost.toString(), new Volume("/data/test")),
            new Bind(srcPathHost.toString(), new Volume("/data/src")),
            new Bind(resultPathHost.toString(), new Volume("/data/result"))
        );

        return generateResult(assignmentId, resultPath, testPathHost);
    }

    private String loadImageConfig(Path workDirectory) throws FileNotFoundException {
        Matcher config = RegexUtil.extractConfig(
            new FileInputStream(workDirectory.resolve("config.txt").toFile()), 
            Pattern.compile(RegexUtil.DTA_TESTCONFIGREGEX));

        if (config == null) {
            config = RegexUtil.extractConfig(
                new FileInputStream(workDirectory.resolve("config.txt").toFile()), 
                Pattern.compile(RegexUtil.TESTCONFIGREGEX));
            
            if (config == null) {
                throw new RuntimeException("Couldn't find repo config for unittest image extraction");
            }
            return config.group(4);
        }
        return config.group(5);
    }

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

        if (!resultFile.exists() || !resultFile.isFile()) {
            LOG.error(String.format("Could not find result file in %s", resultFile.getAbsolutePath()));
            throw new RuntimeException("No result file found");
        }

        LOG.debug("Parse results JSON");
        ObjectMapper objectMapper = new ObjectMapper();
        ResultSummary resultSummary = objectMapper.readValue(resultFile.toURI().toURL(), ResultSummary.class);
        LOG.debug("Result JSON returned at " + resultSummary.timestamp + " with " + resultSummary.results.size() + " test results.");

        LOG.info("Checking for optional test competency profile information...");
        List<TestCompetencyProfile> testCompetencyProfiles = CompetencyAssessmentUtil.readTestCompetencyProfiles(testPathHost);

        if (testCompetencyProfiles != null) {
            LOG.info("Found test competency profiles, generating profile data...");
            resultSummary.overallTestCompetencyProfile = CompetencyAssessmentUtil.packFloats(CompetencyAssessmentUtil.sumTestCompetencyProfiles(testCompetencyProfiles));
            resultSummary.successfulTestCompetencyProfile = CompetencyAssessmentUtil.packFloats(CompetencyAssessmentUtil.sumSuccessfulCompetencyProfiles(testCompetencyProfiles, resultSummary, true));
            
            Path exerciseManifestFile = testPathHost.resolve(CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME);
            if (Files.exists(exerciseManifestFile)) {
                LOG.info("Found exercise competency profiles, generating recommendations...");
                resultSummary.recommendations = recommendNextExercises(assignmentId, testPathHost, testCompetencyProfiles, resultSummary);
            }
        }
        return resultSummary;
    }

    public List<Recommendation> recommendNextExercises(String assignmentId, Path testPathHost, List<TestCompetencyProfile> testCompetencyProfiles, ResultSummary resultSummary)
        throws FileNotFoundException {
    
    String testRepoURL = RegexUtil.extractConfig(
        new FileInputStream(Paths.get(assignmentBasePath, assignmentId + ".txt").toFile()), 
        Pattern.compile(RegexUtil.DTA_TESTCONFIGREGEX)).group(1);

    List<ExerciseCompetencyProfile> exerciseCompetencyProfiles = CompetencyAssessmentUtil.readExerciseCompetencyProfiles(testPathHost);

    int currentTopicIndex = 0;
    float currentDifficulty = 0.0f;
    Map<String, Integer> topicOrder = new HashMap<>();
    int order = 1;

    // Determine currentTopicIndex and set currentDifficulty based on exercise profiles
    for (ExerciseCompetencyProfile e : exerciseCompetencyProfiles) {
        if (!topicOrder.containsKey(e.exerciseTopicName)) {
            topicOrder.put(e.exerciseTopicName, order++);
        }
        if (e.exerciseURL.equals(testRepoURL)) {
            currentTopicIndex = order;
            currentDifficulty = e.difficulty;  // Directly assign to currentDifficulty
        }
    }

    // Sum competencies for unsuccessful tests
    float[] unsuccessful = CompetencyAssessmentUtil.sumSuccessfulCompetencyProfiles(testCompetencyProfiles, resultSummary, false);

    // Filter exercises based on topics and difficulty
    List<ExerciseCompetencyProfile> filteredExercises = filterExercisesByTopicsAndDifficulty(
            exerciseCompetencyProfiles, topicOrder, currentTopicIndex, testRepoURL, currentDifficulty, unsuccessful);

    // Generate recommendations without using lambda expressions
    List<Recommendation> recommendedExercises = new ArrayList<>();
    for (ExerciseCompetencyProfile profile : filteredExercises) {
        float score = calculateScore(profile, unsuccessful, topicOrder, currentDifficulty);
        Recommendation recommendation = new Recommendation(
                profile.exerciseTopicName, 
                profile.exerciseURL, 
                profile.exerciseName, 
                profile.difficulty, 
                score);
        recommendedExercises.add(recommendation);
    }

    // Sort the recommendations using Collections.sort and a comparator
    Collections.sort(recommendedExercises, Recommendation.COMPARE_BY_SCORE);
    
    return recommendedExercises;
}

    public static List<ExerciseCompetencyProfile> filterExercisesByTopicsAndDifficulty(List<ExerciseCompetencyProfile> profiles,
            Map<String, Integer> topicOrder, int currentTopicIndex, String testRepoURL, float currentDifficulty, float[] unsuccessful) {
        
        List<ExerciseCompetencyProfile> filteredExercises = profiles.stream()
                .filter(profile -> topicOrder.get(profile.exerciseTopicName) <= currentTopicIndex)
                .collect(Collectors.toList());

        if (CompetencyAssessmentUtil.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 float calculateScore(ExerciseCompetencyProfile profile, float[] unsuccessful, Map<String, Integer> topicOrder, float currentDifficulty) {
        float score = 1.0f;
        for (int i = 0; i < profile.competencyAssessments.length - 1; i++) {
            score += profile.competencyAssessments[i] * unsuccessful[i];
        }

        score *= profile.difficulty * (0.5f + Math.abs(currentDifficulty - profile.difficulty));
        score *= topicOrder.getOrDefault(profile.exerciseTopicName, 1);
        
        return Math.round(score * 10.0f) / 10.0f;
    }
}