package performa.utils;

import java.util.*;
import oneit.logging.*;
import oneit.objstore.*;
import oneit.objstore.rdbms.filters.*;
import oneit.utils.*;
import oneit.utils.filter.*;
import oneit.utils.math.*;
import oneit.utils.parsers.FieldException;
import performa.orm.*;
import performa.orm.types.*;

/**
 *
 * @author nilu
 */
public class AnalysisEngine 
{
    private static final    Long    MAX_VALID_FACTOR_NUMBER =   49L; //Don't consider unusally high/low answer factors i.e 50/51
    
    public static void analyseAnswers(Candidate candidate, Level level, Set<Answer> answers) throws StorageException, FieldException
    {
        LogMgr.log(JobApplication.LOG, LogLevel.PROCESSING1, "Inside AnalysisEngine --> analyseAnswers for candidate ", candidate, " Level ", level);
        
        if(!answers.isEmpty())
        {
            ObjectTransaction   objTran =   candidate.getTransaction();
            
            if(candidate.getTestAnalysisFor(level) == null)
            {
                Level.LevelPipeLineFactory  levelPipeline   =   level.pipelineLevel();
                Set<Factor>                 levelFactors    =   levelPipeline.toFactors().toFactor().uniqueVals();
                Filter<FactorQuestionLink>  factorFilter    =   FactorQuestionLink.SearchByAll().andFactor(new InFilter(levelFactors));

                //Preloading Data
                Answer.pipesAnswer(answers).toQuestion().toSection().uniqueVals();
                Answer.pipesAnswer(answers).toQuestion().toFactors().toFactor().uniqueVals();
                Factor.pipesFactor(levelFactors).toResults().uniqueVals();
                Factor.pipesFactor(levelFactors).toLevelFactorTypes().uniqueVals();
                levelPipeline.toClassNormalisations().uniqueVals();

                Map<Factor, Integer>    factorScoreMap  =   new HashMap();

                for (Answer answer : answers)
                {
                    Set<FactorQuestionLink> links   =   answer.pipelineAnswer().toQuestion().toFactors(factorFilter).uniqueVals();

                    for (FactorQuestionLink link : links)
                    {
                        int     factorScore =   0;
                        Factor  factor      =   link.getFactor();

                        if(factor.getID().longID() <= MAX_VALID_FACTOR_NUMBER)  //Don't consider unusally high/low answer factors i.e 50/51
                        {
                            if(factorScoreMap.containsKey(factor))
                            {
                                factorScore =   factorScoreMap.get(factor);
                            }

                            if(link.isTrue(link.getReverseScore()))
                            {
                                if(answer.getQuestion().getQuestionType() == QuestionType.IPSATIVE)
                                {
                                    factorScore += (10 - answer.getAnswerNo());
                                }
                                else
                                {
                                    factorScore += (8 - answer.getAnswerNo());
                                }
                            }
                            else
                            {
                                factorScore +=  StringUtils.subNulls(answer.getAnswerNo(), 0);
                            }
                            factorScoreMap.put(factor, factorScore);
                        }
                    }
                }

                //Unusually High Answers/Unusually Low Answers
                Filter<Question>                questFilter             =   Question.SearchByAll().andHighLowFactor(new IsNotNullFilter());
                MultiHashtable<Factor, Answer>  highLowAnswersByFactor  =   new MultiHashtable();

                highLowAnswersByFactor.groupValues(answers, Answer.pipesAnswer().toQuestion(questFilter).toHighLowFactor());

                for(Factor factor : highLowAnswersByFactor.keySet())
                {
                    if(factor != null)
                    {
                        double  answerTotal =   Statistics.sum(Answer.pipesAnswer(highLowAnswersByFactor.getValuesForKeyNN(factor)).toAnswerNo().vals());

                        factorScoreMap.put(factor, Double.valueOf(answerTotal).intValue());
                    }
                }

                TestAnalysis    testAnalysis    =   TestAnalysis.createTestAnalysis(objTran);

                level.addToTestAnalysises(testAnalysis);
                candidate.addToTestAnalysises(testAnalysis);

                for(Factor factor : factorScoreMap.keySet())
                {
                    int score   =   factorScoreMap.get(factor);

                    LogMgr.log(JobApplication.LOG, LogLevel.PROCESSING1, "Candidate:", candidate, " Factor:", factor, " Score:", score);

                    Filter<FactorScoreResult>   factorScoreFilter   =   FactorScoreResult.SearchByAll().andLevel(new EqualsFilter<>(level))
                                                                                                    .andFromScore(new LessThanEqualFilter<>(score))
                                                                                                    .andToScore(new GreaterThanEqualFilter<>(score));

                    List<FactorScoreResult>     factorScoreResults  =   (List<FactorScoreResult>) factor.pipelineFactor().toResults(factorScoreFilter).vals();

                    if(factorScoreResults != null && !factorScoreResults.isEmpty())
                    {
                        FactorScoreResult   factorScoreResult   =   factorScoreResults.get(0);

                        FactorScore factorScore =   FactorScore.createFactorScore(objTran);

                        factorScore.setFactor(factor);
                        factorScore.setLevel(level);
                        factorScore.setScore(score);
                        factorScore.setColorCode(factorScoreResult.getColorCode());
                        factorScore.setColorRank(factorScoreResult.getColorCode() != null ? factorScoreResult.getColorCode().getColorRank() : 0);
                        factorScore.setNarrative(factorScoreResult.getNarrative());

                        /**
                         *  Weighted Score.
                         * 
                         *  10 where color rank = 1 and flag = Primary
                         *  5 where color rank = 1 and flag = Secondary
                         *  4 where color rank = 2 and flag = Primary
                         *  2 where color rank = 2 and flag = Secondary
                        */
                        Integer weightedScore   =   0;

                        if(factorScoreResult.getColorCode() == ColorCode.GREEN || factorScoreResult.getColorCode() == ColorCode.AMBER)
                        {
                            LevelFactorType levelFactorType =   level.getLevelFactorType(factor);

                            if(levelFactorType != null && levelFactorType.getTypeFlag() != null)
                            {
                                if(factorScoreResult.getColorCode() == ColorCode.GREEN)//Color Code: Green  --> Color Rank = 1
                                {
                                    weightedScore   =   (levelFactorType.getTypeFlag() == TypeFlag.PRIMARY) ? 10 : 5;
                                }
                                else    //Color Code: Amber  --> Color Rank = 2
                                {
                                    weightedScore   =   (levelFactorType.getTypeFlag() == TypeFlag.PRIMARY) ? 4 : 2;
                                }
                            }
                        }
                        factorScore.setWghtdScore(weightedScore);

                        testAnalysis.addToFactorScores(factorScore);
                    }
                }

                Set<FactorLevelLink>    factorLevelLinks    =   levelPipeline.toFactors().uniqueVals();

                for(FactorClass factorClass : level.pipelineLevel().toLevelClassCriterias().toFactorClass().uniqueVals())
                {
                    Integer classScore          =   0;
                    Integer wghtdclassScore     =   0;
                    Integer maxWghtdClassScore  =   0;

                    Filter<FactorLevelLink> factorLevelLinkFilter   =   FactorLevelLink.SearchByAll().andFactorClass(new EqualsFilter(factorClass));
                    List<FactorLevelLink>   factorLevelLinksForClass=   (List<FactorLevelLink>) CollectionFilter.filter(factorLevelLinks, factorLevelLinkFilter);

                    for(FactorLevelLink factorLevelLink : factorLevelLinksForClass)
                    {
                        Filter<FactorScore> factorScoreFilter       =   FactorScore.SearchByAll().andFactor(new EqualsFilter(factorLevelLink.getFactor())).andLevel(new EqualsFilter(factorLevelLink.getLevel()));
                        List<FactorScore>   filteredFactorScores    =   (List<FactorScore>) CollectionFilter.filter(testAnalysis.getFactorScoresSet(), factorScoreFilter);

                        LevelFactorType levelFactorType =   factorLevelLink.getLevel().getLevelFactorType(factorLevelLink.getFactor());
                        TypeFlag        typeFlag        =   levelFactorType.getTypeFlag();
                        Integer         multiplier      =   0;

                        if(typeFlag == TypeFlag.PRIMARY)
                        {
                            multiplier  =   2;
                        }
                        else if(typeFlag == TypeFlag.SECONDARY)
                        {
                            multiplier  =   1;
                        }

                        for(FactorScore factorScore : filteredFactorScores)
                        {
                            if(factorScore.getColorCode() == ColorCode.GREEN)
                            {
                                classScore      +=  10;
                                wghtdclassScore +=  (multiplier * 50);
                            }
                            else if(factorScore.getColorCode() == ColorCode.AMBER)
                            {
                                classScore      +=  5;
                                wghtdclassScore +=  (multiplier * 25);
                            }
                            else if(factorScore.getColorCode() == ColorCode.RED)
                            {
                                classScore      +=  1;
                                wghtdclassScore +=  (multiplier * 5);
                            }
                        }
                        maxWghtdClassScore  +=  (multiplier * 50);
                    }

                    ClassNormalisation  classNormalisation  =   level.getClassNormalisationFor(factorClass);
                    CandidateClassScore candidateClassScore =   CandidateClassScore.createCandidateClassScore(objTran);

                    candidateClassScore.setFactorClass(factorClass);
                    candidateClassScore.setClassScore(classScore);
                    candidateClassScore.setWghtdClassScore(wghtdclassScore);
                    candidateClassScore.setMaxWghtdClassScore(maxWghtdClassScore);
                    candidateClassScore.setColorCode(NullArith.lessThan(classScore, classNormalisation.getLeftMeanScore()) ? ColorCode.RED : (NullArith.lessThan(classScore, classNormalisation.getMeanScore()) ? ColorCode.AMBER : ColorCode.GREEN));
                    candidateClassScore.setWghtdColorCode(NullArith.lessThan(wghtdclassScore, classNormalisation.getWghtLeftMeanScore()) ? ColorCode.RED : (NullArith.lessThan(wghtdclassScore, classNormalisation.getWghtMeanScore()) ? ColorCode.AMBER : ColorCode.GREEN));
                    testAnalysis.addToCandidateClassScores(candidateClassScore);
                }
            }
            else
            {
                LogMgr.log(JobApplication.LOG, LogLevel.PROCESSING1, "AnalysisEngine --> analyseAnswers skipped as already present for Candidate ", candidate, " Level:", level);
            }
        }
        else
        {
            LogMgr.log(JobApplication.LOG, LogLevel.PROCESSING1, "AnalysisEngine --> analyseAnswers skipped as no answers found for Candidate ", candidate, " Level:", level);
        }
        LogMgr.log(JobApplication.LOG, LogLevel.PROCESSING1, "AnalysisEngine --> analyseAnswers completed for Candidate ", candidate, " Level:", level);
    }
    
    public static Map<CultureClass, Long> getCultureFit(Set<CultureCriteriaAnswer> cultureCriteriaAnswers, Job job)
    {
        LogMgr.log(JobApplication.LOG, LogLevel.PROCESSING1, "AnalysisEngine --> getCultureFit called");
        
        //Preloading data
        CultureCriteriaAnswer.pipesCultureCriteriaAnswer(cultureCriteriaAnswers).toSelectedQuestion().toNarratives().toCultureElementRating().uniqueVals();
        CultureCriteriaAnswer.pipesCultureCriteriaAnswer(cultureCriteriaAnswers).toCultureElement().uniqueVals();
        
        Map<CultureClass, Integer>  scoreMap    =   new HashMap();
        Map<CultureClass, Integer>  maxScoreMap =   new HashMap();
        
        for(CultureClass cultureClass : CultureClass.getCultureClassArray())
        {
            scoreMap.put(cultureClass, 0);
            maxScoreMap.put(cultureClass, 0);
        }
        
        
        for(CultureCriteriaAnswer answer : cultureCriteriaAnswers)
        {
            CultureCriteria criteria    =   answer.getCriteriaJob(job);
            
            if(criteria != null && criteria.getImportance() != null && criteria.getImportance() != Importance.NOT_APPLICABLE)
            {
                CultureElementRating    rating  =   criteria.getCultureElementRating();
                
                if(rating != null && rating.getCultureElement() != null && rating.getCultureElement().getCultureClass() != null)
                {
                    CultureElement          element         =   rating.getCultureElement();
                    CultureClass            cultureClass    =   element.getCultureClass();
                    CultureElementQuestion  question        =   answer.getSelectedQuestion();
                    Integer                 weightingScore  =   criteria.getImportance().getWeightingScore();
                    Integer                 ratingScore     =   0;

                    if(question != null)
                    {
                        Filter              narrativeFilter =   CultureNarrative.SearchByAll().andCultureElementRating(new EqualsFilter(rating));
                        CultureNarrative    narrative       =   (CultureNarrative) question.pipelineCultureElementQuestion().toNarratives(narrativeFilter).val();

                        if(narrative != null && narrative.getColorCode() != null)
                        {
                            ratingScore =   narrative.getColorCode().getRatingScore();
                        }
                    }
                    
                    int score       =   (weightingScore * ratingScore);
                    int maxScore    =   (weightingScore * ColorCode.GREEN.getRatingScore());

                    scoreMap.put(cultureClass, (scoreMap.get(cultureClass) + score));
                    maxScoreMap.put(cultureClass, (maxScoreMap.get(cultureClass) + maxScore));
                }
            }
        }

        Map<CultureClass, Long> cultureFitMap   =   getFinalFitMap(scoreMap, maxScoreMap);
                
        LogMgr.log(JobApplication.LOG, LogLevel.PROCESSING1, "AnalysisEngine --> getCultureFit completed");
        
        return cultureFitMap;
    }
    
    private static <T> Map<T, Long> getFinalFitMap(Map<T, Integer> scoreMap, Map<T, Integer> maxScoreMap)
    {
        Map<T, Long>    fitMap          =   new HashMap();
        int             totalScore      =   0;
        int             totalMaxScore   =   0;
        
        for(T key : scoreMap.keySet())
        {
            int score       =   scoreMap.get(key);
            int maxScore    =   maxScoreMap.get(key);
            
            LogMgr.log(JobApplication.LOG, LogLevel.DEBUG1, "getCultureFit score for ", key, " ", score, " max:", maxScore);
            
            totalScore      +=  score;
            totalMaxScore   +=  maxScore;
            
            if(maxScore != 0)       //To prevent divide by zero
            {
                fitMap.put(key, Math.round(NullArith.divide(score, maxScore) * 100));
            }
            else
            {
                fitMap.put(key, 0L);
            }
        }
        
        if(totalMaxScore != 0)
        {
            fitMap.put(null, Math.round(NullArith.divide(totalScore, totalMaxScore) * 100));  //Using NULL key for final total
        }
        else
        {
            fitMap.put(null, 0L);  //Using NULL key for final total
        }
        return fitMap;
    }
    
    public static Map<Importance, Long> getRequirementFit(Set<AssessmentCriteriaAnswer> assessmentCriteriaAnswers)
    {
        LogMgr.log(JobApplication.LOG, LogLevel.PROCESSING1, "AnalysisEngine --> getRequirementFit called");
        
        Map<Importance, Integer>    scoreMap    =   new HashMap();
        Map<Importance, Integer>    maxScoreMap =   new HashMap();
        
        for(Importance importance : Importance.getImportanceArray())
        {
            if(importance.getWeightingScore() > 0)      //Skiping NOT_APPLICABLE
            {
                scoreMap.put(importance, 0);
                maxScoreMap.put(importance, 0);
            }
        }
        
        for(AssessmentCriteriaAnswer answer : assessmentCriteriaAnswers)
        {
            if(answer.getAssessmentCriteria() != null && answer.getAssessmentCriteria().getImportance() != null)
            {
                Importance  importance  =   answer.getAssessmentCriteria().getImportance();
                int         maxScore    =   importance.getWeightingScore();
                int         score       =   answer.isCorrectAnswer() ? maxScore : 0;

                scoreMap.put(importance, (scoreMap.get(importance) + score));
                maxScoreMap.put(importance, (maxScoreMap.get(importance) + maxScore));
            }
        }
        
        Map<Importance, Long>  requirementFitMap =   getFinalFitMap(scoreMap, maxScoreMap);
        
        LogMgr.log(JobApplication.LOG, LogLevel.PROCESSING1, "AnalysisEngine --> getRequirementFit completed");
        
        return requirementFitMap;
    }
    
    public static Map<FactorClass, Long> getRoleFitPercentage(Candidate candidate, Level level)
    {
        LogMgr.log(JobApplication.LOG, LogLevel.PROCESSING1, "AnalysisEngine --> getRoleFit called for candidate ", candidate, " Level ", level);
        
        TestAnalysis                testAnalysis        =   candidate.getTestAnalysisFor(level);
        Map<FactorClass, Integer>   scoreMap            =   new HashMap();
        Map<FactorClass, Integer>   maxScoreMap         =   new HashMap();
        
        if(testAnalysis != null && testAnalysis.getCandidateClassScoresCount() > 0)
        {
            for(LevelClassCriteria levelClassCriteria : level.getLevelClassCriteriasSet())
            {
                FactorClass                 factorClass                 =   levelClassCriteria.getFactorClass();
                Filter<CandidateClassScore> candidateClassScoreFilter   =   CandidateClassScore.SearchByAll().andFactorClass(new EqualsFilter(factorClass));
                CandidateClassScore         candidateClassScore         =   testAnalysis.pipelineTestAnalysis().toCandidateClassScores(candidateClassScoreFilter).val();
                Integer                     score                       =   0;
                Importance                  importance                  =   levelClassCriteria.getImportance();

                maxScoreMap.put(factorClass, importance != null ? importance.getWeightingScore() * 10 : 0);
                
                if(candidateClassScore != null)
                {
                    ColorCode   colorCode   =   candidateClassScore.getWghtdColorCode();
                    
                    score   =   (importance != null ? importance.getWeightingScore() : 0) * (colorCode != null ? colorCode.getRatingScore() : 0);
                }
                scoreMap.put(factorClass, score);
            }
        }
        Map<FactorClass, Long>  roleFitMap  =   getFinalFitMap(scoreMap, maxScoreMap);
        
        LogMgr.log(JobApplication.LOG, LogLevel.PROCESSING1, "AnalysisEngine --> getRoleFit completed for candidate ", candidate, " Level ", level);
        
        return roleFitMap;
    }
    
    public static Map<FactorClass, Tuple.T2<Double, ColorCode>> getRoleFitSuitability(Candidate candidate, Level level)
    {
        LogMgr.log(JobApplication.LOG, LogLevel.PROCESSING1, "AnalysisEngine --> getRoleFitSuitability called for candidate ", candidate, " Level ", level);
        
        TestAnalysis                                    testAnalysis    =   candidate.getTestAnalysisFor(level);
        Map<FactorClass, Tuple.T2<Double, ColorCode>>   scoreMap        =   new HashMap();
        
        if(testAnalysis != null && testAnalysis.getCandidateClassScoresCount() > 0)
        {
            for(LevelClassCriteria levelClassCriteria : level.getLevelClassCriteriasSet())
            {
                FactorClass                 factorClass                 =   levelClassCriteria.getFactorClass();
                Filter<CandidateClassScore> candidateClassScoreFilter   =   CandidateClassScore.SearchByAll().andFactorClass(new EqualsFilter(factorClass));
                CandidateClassScore         candidateClassScore         =   testAnalysis.pipelineTestAnalysis().toCandidateClassScores(candidateClassScoreFilter).val();
                ClassNormalisation          classNormalisation          =   level.getClassNormalisationFor(factorClass);
                Double                      score                       =   0d;
                ColorCode                   colorCode                   =   null;
                
                if(candidateClassScore != null && classNormalisation != null && NullArith.greaterThan(classNormalisation.getWghtStddevScore(), 0d))
                {
                    score       =   NullArith.round(((10 * ((candidateClassScore.getWghtdClassScore() - classNormalisation.getWghtMeanScore()) / classNormalisation.getWghtStddevScore())) + 50), 1);
                    colorCode   =   candidateClassScore.getColorCode();
                }
                scoreMap.put(factorClass, new Tuple.T2(score, colorCode));
            }
            scoreMap.put(null, getSuitabilityScore(testAnalysis));
        }
        LogMgr.log(JobApplication.LOG, LogLevel.PROCESSING1, "AnalysisEngine --> getRoleFitSuitability completed for candidate ", candidate, " Level ", level);
        
        return scoreMap;
    }
    
    public static Tuple.T2<Double, ColorCode> getSuitabilityScore(TestAnalysis testAnalysis)
    {
        LogMgr.log(JobApplication.LOG, LogLevel.PROCESSING1, "AnalysisEngine --> getSuitabilityScore called for testAnalysis ", testAnalysis);
        
        Double      totalScore          =   testAnalysis.getWghtdLevelScore();
        Double      wghtdMeanScore      =   0d;
        Double      wghtdStdDevScore    =   0d;
        Double      suitabilityScore    =   0d;
        ColorCode   colorCode           =   null;
        
        LevelNormalisation  levelNormalisation  =   testAnalysis.getLevel().getLevelNormalisation();
        
        if(levelNormalisation != null)
        {
            wghtdMeanScore      =   levelNormalisation.getWghtMeanScore();
            wghtdStdDevScore    =   levelNormalisation.getWghtStddevScore();
        }
        
        if(wghtdStdDevScore > 0d)
        {
            suitabilityScore    =   NullArith.round(((10 * ((totalScore - wghtdMeanScore) / wghtdStdDevScore)) + 50), 2);
        }
        
        if(totalScore >= wghtdMeanScore)
        {
            colorCode   =   ColorCode.GREEN;
        }
        else if(totalScore < (wghtdMeanScore - wghtdStdDevScore))
        {
            colorCode   =   ColorCode.RED;
        }
        else
        {
            colorCode   =   ColorCode.AMBER;
        }
        LogMgr.log(JobApplication.LOG, LogLevel.PROCESSING1, "AnalysisEngine --> getSuitabilityScore completed for testAnalysis ", testAnalysis, " Score:", suitabilityScore, " Color:", colorCode);
        
        return new Tuple.T2(suitabilityScore, colorCode);
    }
    
    public static Map<FactorLevelLink, Map> getFactorScoreDetails(Candidate candidate, Level level, FactorClass factorClass)
    {
        LogMgr.log(JobApplication.LOG, LogLevel.PROCESSING1, "AnalysisEngine --> getFactorScoreDetails called for candidate: ", candidate, " Level: ", level, " factorClass: ", factorClass);
        
        Map<FactorLevelLink, Map>   factorDetails   =   new LinkedHashMap<>();
        TestAnalysis                testAnalysis    =   candidate.getTestAnalysisFor(level);
        
        if(testAnalysis != null)
        {
            List<FactorLevelLink>   factorLevelLinks    =   level.getSortedFactorLinks(factorClass);
            
            for(FactorLevelLink factorLevelLink : factorLevelLinks)
            {
                Map                             factorLinkDetails   =   new HashMap();
                Map<FactorScoreResult, Double>  factorScoreDetails  =   new LinkedHashMap();
                List<FactorScoreResult>         factorScoreResults  =   level.getSortedFactorScoreResults(factorLevelLink.getFactor());
                
                Integer minScore    =   factorScoreResults.get(0).getFromScore();
                Integer maxScore    =   factorScoreResults.get(factorScoreResults.size() - 1).getToScore();
                Double  totalLength =   NullArith.subtract(maxScore, minScore, 0d) + 1;
                
                for(FactorScoreResult scoreResult : factorScoreResults)
                {
                    factorScoreDetails.put(scoreResult, (NullArith.divide(NullArith.subtract(scoreResult.getToScore(), scoreResult.getFromScore(), 0d) + 1, totalLength) * 100));
                }
                
                FactorScore factorScore =   testAnalysis.getFactorScoreFor(factorLevelLink.getFactor());
                
                if(factorScore != null && factorScore.getScore() != null)
                {
                    factorLinkDetails.put("factorScoreDetails", factorScoreDetails);
                    factorLinkDetails.put("factorScore", factorScore);
                    factorLinkDetails.put("scoreLeftMargin", (NullArith.divide(NullArith.subtract(factorScore.getScore(), minScore, 0d), totalLength) * 100));
                }
                factorDetails.put(factorLevelLink, factorLinkDetails);
            }
        }
        LogMgr.log(JobApplication.LOG, LogLevel.PROCESSING1, "AnalysisEngine --> getFactorScoreDetails completed for candidate: ", candidate, " Level: ", level, " factorClass: ", factorClass);
        
        return factorDetails;
    }
}