"""This module holds exporters for ``assessments`` app."""

import io
from itertools import chain, groupby

from openpyxl import Workbook

from django.utils import formats
from django.utils.translation import ugettext as _

from mtp.apps.assessments.models import (
    AssessableActivityUserScore,
    CertificationProgramUserStatus,
    CompetencyTree,
    CompetencyTreeUserScore,
    CompetencyUserScore,
    Curriculum,
    DimensionUserScore,
    UserLessonRecommendation,
)
from mtp.apps.common.utils import filter_list_of_dicts, get_dict_in_list, sort_list_of_dicts
from mtp.apps.trainings.models import TrainingActivity


class CompetenciesScoresExporter:
    """Exporter to export competencies scores.

    Attributes
    ----------
    competencies: list(dict)
        The competencies related to the provided tree.
    dimensions: list(dict)
        The dimensions related to the provided tree.
    competencies_scores: list(dict)
        The competencies scores related to the provided sessions.
    dimensions_scores: list(dict)
        The dimensions scores related to the provided sessions.

    """

    def __init__(self, sessions, tree):
        """Initialize instance attributes.

        Parameters
        ----------
        sessions: iterable(TrainingSession)
            The sessions for which we want to export the competencies scores.
        tree: CompetencyTree
            The tree for which we want to export the competencies and dimensions.

        """

        self.competencies = tree._get_competencies_list()
        self.dimensions = tree._get_dimensions_list()
        self.competencies_scores = self._get_competencies_scores(sessions)
        self.dimensions_scores = self._get_dimensions_scores(sessions)

    @staticmethod
    def _get_competencies_scores(sessions):
        """Get the competencies scores for the provided sessions.

        Parameters
        ----------
        sessions: iterable(TrainingSession)
            The sessions list to use to filter ``CompetencyUserScore``.

        Returns
        -------
        list(dict)
            Each dict contains the following keys:
             - ``competency_id``: the identifier of the evaluated competency
             - ``user_id``: the identifier of the trainee
             - ``score``: the score of the trainee
             - ``session_id``: the identifier of the current session

        """

        return CompetencyUserScore.objects.filter(
            session__in=sessions,
        ).distinct().values(
            'competency_id', 'user_id', 'score', 'session_id'
        ).order_by('competency__sort_order')

    @staticmethod
    def _get_dimensions_scores(sessions):
        """Get the dimensions scores for the provided sessions.

        Parameters
        ----------
        sessions: iterable(TrainingSession)
            The sessions list to use to filter ``DimensionUserScore``.

        Returns
        -------
        list(dict)
            Each dict contains the following keys:
             - ``dimension_id``: the identifier of the evaluated dimension
             - ``dimension__competency_id``: the identifier of the evaluated competency
             - ``user_id``: the identifier of the trainee
             - ``score``: the score of the trainee
             - ``session_id``: the identifier of the current session

        """

        return DimensionUserScore.objects.filter(
            session__in=sessions,
        ).distinct().values(
            'dimension_id', 'dimension__competency_id', 'user_id', 'score', 'session_id'
        ).order_by('dimension__sort_order')

    def get_competencies_names(self):
        """Get names from ``self.competencies`` and ``self.dimensions``.

        Returns
        -------
        list(str)
            The list containing the names of competencies and dimensions.

        """

        names = []

        for competency in self.competencies:

            dimensions_names = [
                dimension['name']
                for dimension in filter_list_of_dicts(
                    self.dimensions, 'competency_id', competency['id']
                )
            ]

            names += [competency['name']] + dimensions_names

        return names

    def get_competencies_scores_for_trainee(self, session_id, trainee_id):
        """Get the competencies scores for a trainee during a session.

        Parameters
        ----------
        session_id: str
            The id of the session for which we want to export scores.
        trainee_id: str
            The id of the trainee for which we want to find the score.

        Returns
        -------
        list(int)
            The trainee competencies scores.

        """

        trainee_competency_scores = filter_list_of_dicts(
            filter_list_of_dicts(self.competencies_scores, 'user_id', trainee_id),
            'session_id', session_id
        )

        trainee_dimension_scores = filter_list_of_dicts(
            filter_list_of_dicts(self.dimensions_scores, 'user_id', trainee_id),
            'session_id', session_id
        )

        trainee_scores = []

        for competency_score in trainee_competency_scores:

            dimension_scores = [
                dimension_score['score']
                for dimension_score in filter_list_of_dicts(
                    trainee_dimension_scores,
                    'dimension__competency_id',
                    competency_score['competency_id']
                )
            ]

            trainee_scores += [competency_score['score']] + dimension_scores

        return trainee_scores


class ActivitiesScoresExporter:
    """Exporter to export activities scores.

    Attributes
    ----------
    activities: list(dict)
        The activities related to the provided sessions.
    activities_scores: list(dict)
        The dimensions scores related to the provided sessions.

    """

    def __init__(self, sessions):
        """Initialize instance attributes.

        Parameters
        ----------
        sessions: iterable(TrainingSession)
            The sessions for which we want to export the activities scores.

        """

        self.activities = self._get_original_sessions_activities(sessions)
        self.activities_scores = self._get_activities_scores(sessions)

    @staticmethod
    def _get_original_sessions_activities(sessions):
        """Get a list of original assessable activities associated with the provided session list.

        Parameters
        ----------
        sessions: iterable(TrainingSession)
            The sessions for which we want to fetch the activities.

        Notes
        -----
        We want every element of this list to be distinct.

        Returns
        -------
        list(dict)
            Each dict contains the following keys:
             - ``id``: the identifier of the activity
             - ``name``: the name of the activity
             - ``order``: the sort order of this activity

        """

        original_activities = []

        # We fetch the list of assessable activities in provided sessions.
        assessable_activities = TrainingActivity.objects.non_deleted().filter(
            training__training_sessions__in=sessions,
            is_assessing=True,
        ).distinct()

        # We separate the activities acquisition to distinct activities
        # with the same original version (when it is not null).
        activities_with_original = assessable_activities.filter(
            original_version__isnull=False
        ).distinct('original_version_id').prefetch_related('original_version')

        activities_without_original = assessable_activities.filter(
            original_version__isnull=True
        )

        activities = list(activities_with_original) + list(activities_without_original)
        for activity in activities:
            original_activity = activity.original_version or activity
            original_activities.append({
                'id': original_activity.id,
                'name': original_activity.name,
                'order': original_activity.sort_order,
            })

        # Finally we sort the list to make its order
        # deterministic across multiple calls.
        return sort_list_of_dicts(original_activities, 'order')

    @staticmethod
    def _get_activities_scores(sessions):
        """Get the activities scores for the provided sessions.

        Parameters
        ----------
        sessions: iterable(TrainingSession)
            The sessions list to use to filter ``AssessableActivityUserScore``.

        Returns
        -------
        list(dict)
            Each dict contains the following keys:
             - ``activity_id``: the identifier of the evaluated activity
             - ``user_id``: the identifier of the trainee
             - ``score``: the score of the trainee
             - ``session_id``: the identifier of the current session

        """

        return [
            {
                'activity_id': activity_score.activity.original_version_id
                               or activity_score.activity_id,
                'user_id': activity_score.user_id,
                'score': activity_score.score,
                'session_id': activity_score.session.id,
            }
            for activity_score in AssessableActivityUserScore.objects.filter(
                session__in=sessions,
            ).distinct().order_by('activity_order')
        ]

    def get_activities_names(self):
        """Get names from ``self.activities``.

        Returns
        -------
        list(str)
            The list containing the names of activities.

        """

        return [activity['name'] for activity in self.activities]

    def get_activities_scores_for_trainee(self, session_id, trainee_id):
        """Get the activities scores for a trainee.

        Parameters
        ----------
        session_id: str
            The id of the session for which we want to export scores.
        trainee_id: str
            The id of the trainee for which we want to find the score.

        Returns
        -------
        list(int)
            The trainee activities scores.

        """

        trainee_activity_scores = filter_list_of_dicts(
            filter_list_of_dicts(self.activities_scores, 'session_id', session_id),
            'user_id',
            trainee_id
        )

        return [
            get_dict_in_list(
                trainee_activity_scores, 'activity_id', activity['id'], {}
            ).get('score', 0)
            for activity in self.activities
        ]


class CertificationExporter:
    """Exporter to export certification scores.

    Attributes
    ----------
    certifications: list
        The certifications related to the provided sessions.
    global_scores: list
        The global scores related to the provided sessions.

    """

    def __init__(self, sessions):
        """Initialize instance attributes.

        Parameters
        ----------
        sessions: iterable(TrainingSession)
            The sessions for which we want to export the certifications and global scores.

        """

        self.certifications = self._get_certifications(sessions)
        self.global_scores = self._get_global_scores(sessions)

    @staticmethod
    def _get_certifications(sessions):
        """Get the certifications for the provided sessions.

        Parameters
        ----------
        sessions: iterable(TrainingSession)
            The sessions list to use to filter ``CertificationProgramUserStatus``.

        Returns
        -------
        list(dict)
            Each dict contains the following keys:
             - ``user_id``: the identifier of the trainee
             - ``status__name``: the status name of the certification
             - ``session_id``: the identifier of the current session

        """

        return CertificationProgramUserStatus.objects.non_deleted().filter(
            session__in=sessions,
        ).distinct().values('user_id', 'status__name', 'session_id')

    @staticmethod
    def _get_global_scores(sessions):
        """Get the global scores for the provided sessions.

        Parameters
        ----------
        sessions: iterable(TrainingSession)
            The sessions list to use to filter ``CompetencyTreeUserScore``.

        Returns
        -------
        list(dict)
            Each dict contains the following keys:
             - ``user_id``: the identifier of the trainee
             - ``score``: the competency tree score of the trainee
             - ``session_id``: the identifier of the current session

        """

        return CompetencyTreeUserScore.objects.filter(
            session__in=sessions,
        ).distinct().values('user_id', 'score', 'session_id')

    def get_global_score_for_trainee(self, session_id, trainee_id):
        """Get the global score for a trainee.

        Parameters
        ----------
        session_id: str
            The id of the session for which we want to export scores.
        trainee_id: str
            The id of the trainee for which we want to find the score.

        Returns
        -------
        int
            The trainee global score.

        """

        return get_dict_in_list(
            filter_list_of_dicts(self.global_scores, 'session_id', session_id),
            'user_id',
            trainee_id,
            {}
        ).get('score', 0)

    def get_certification_for_trainee(self, session_id, trainee_id):
        """Get the certification for a trainee.

        Parameters
        ----------
        session_id: str
            The id of the session for which we want to export certification status.
        trainee_id: str
            The id of the trainee for which we want to find the certification status.

        Returns
        -------
        str
            The trainee certification status name.

        """

        return get_dict_in_list(
            filter_list_of_dicts(self.certifications, 'session_id', session_id),
            'user_id',
            trainee_id,
            {}
        ).get('status__name')


class RecommendationsExporter:
    """Exporter to export recommendations levels.

    Attributes
    ----------
    lessons: list
        The lessons related to the provided curriculum.
    recommendations: list
        The recommendations related to the provided sessions.

    """

    def __init__(self, sessions, curriculum):
        """Initialize instance attributes.

        Parameters
        ----------
        sessions: iterable(TrainingSession)
            The sessions for which we want to export the recommendations.
        curriculum: Curriculum
            The curriculum for which we want to export the lessons.

        """

        self.lessons = self._get_lessons(curriculum)
        self.recommendations = self._get_recommendations(sessions)

    @staticmethod
    def _get_lessons(curriculum):
        """Get the lessons for the provided tree.

        Parameters
        ----------
        curriculum: Curriculum
            The curriculum to use to fetch lessons.

        Returns
        -------
        list(dict)
            If no curriculum exists, we have no lessons, we return an empty list.
            Else each dict contains the following keys:
             - ``id``: the identifier of the lesson
             - ``name``: the name of the lesson

        """

        if not curriculum:
            return []

        return curriculum.lessons.non_deleted().filter(is_active=True).values(
            'id',
            'name',
        ).order_by('name')

    @staticmethod
    def _get_recommendations(sessions):
        """Get the recommendations for the provided tree.

        Parameters
        ----------
        sessions: iterable(TrainingSession)
            The sessions list to use to filter ``UserLessonRecommendation``.

        Returns
        -------
        list(dict)
            Each dict contains the following keys:
             - ``user_id``: the identifier of the trainee
             - ``lesson__name``: the recommended lesson name
             - ``level__name``: the recommended level name
             - ``session_id``: the identifier of the current session

        """

        return UserLessonRecommendation.objects.non_deleted().filter(
            certification__session__in=sessions,
        ).distinct().order_by('lesson__name').values(
            'certification__user_id', 'lesson__name', 'level__name', 'certification__session_id'
        )

    def get_lessons_names(self):
        """Get names from ``self.lessons``.

        Returns
        -------
        list(str)
            The list containing the names of lessons.

        """

        return [lesson['name'] for lesson in self.lessons]

    def get_lessons_levels_for_trainee(self, session_id, trainee_id):
        """Get the lesson levels for a trainee.

        Parameters
        ----------
        session_id: str
            The id of the session for which we want to export levels.
        trainee_id: str
            The id of the trainee for which we want to find the level.

        Returns
        -------
        list(str)
            The trainee lessons levels.

        """

        trainee_recommendations = filter_list_of_dicts(
            filter_list_of_dicts(self.recommendations, 'certification__session_id', session_id),
            'certification__user_id',
            trainee_id
        )

        return [
            get_dict_in_list(
                trainee_recommendations, 'lesson__name', lesson['name'], {}
            ).get('level__name')
            for lesson in self.lessons
        ]


class AssessmentScoresExporter:
    """This class allow to export assessments scores.

    Attributes
    ----------
    sessions: list(TrainingSession)
        The training session list we want to export the user scores from.
    activities_exporter: ActivitiesScoresExporter
        The exporter to extract activities scores.
    competencies_exporter: CompetenciesScoresExporter
        The exporter to extract competencies scores.
    certifications_exporter: CertificationExporter
        The exporter to extract certifications.
    recommendations_exporter: RecommendationsExporter
        The exporter to extract recommendations.

    """

    def __init__(self, sessions=None):
        """Initialize instance attributes.

        Parameters
        ----------
        sessions: iterable(TrainingSession)
            The sessions for which we want to export the assessment scores.

        """

        self.sessions = [
            session
            for session in sessions or []
            if session.is_closed and session.is_assessing and session.users.exists()
        ]

        self.activities_exporter = None
        self.competencies_exporter = None
        self.certifications_exporter = None
        self.recommendations_exporter = None

    def _initialize_exporters(self, sessions, tree, curriculum):
        """Initialize exporters according the sessions, the associated tree and curriculum.

        Parameters
        ----------
        sessions: iterable(TrainingSession)
            The sessions for which we want to export scores.
        tree: CompetencyTree
            The tree for which we want to export scores.
        curriculum: Curriculum
            The curriculum for which we want to export lessons and recommendations.

        """

        self.activities_exporter = ActivitiesScoresExporter(sessions)
        self.competencies_exporter = CompetenciesScoresExporter(sessions, tree)
        self.certifications_exporter = CertificationExporter(sessions)
        self.recommendations_exporter = RecommendationsExporter(sessions, curriculum)

    def _get_header_row(self):
        """Get the header row to insert in the scores table.

        Returns
        -------
        list(str)
            The header row.

        """

        header_row = []

        # We add the trainee fields
        header_row.extend([
            _("Business unit"), _("Session date"), _("Assessor"), _("Username"), _("Full name"),
            _("Global score"), _("Certification")
        ])

        # We add the activity names
        header_row.extend(self.activities_exporter.get_activities_names())

        # Then the competency and nested dimension names
        header_row.extend(self.competencies_exporter.get_competencies_names())

        # And finally the lesson names
        header_row.extend(self.recommendations_exporter.get_lessons_names())

        return header_row

    def _build_sessions_scores(self, sessions, tree, curriculum=None):
        """Build sessions scores according the provided tree.

        This method builds a list of aggregated scores for all trainees in provided sessions.

        Notes
        -----
        That list is essentially ready to dump into a .csv or .xls(x) file.

        Parameters
        ----------
        sessions: iterable(TrainingSession)
            The sessions for which we want to export scores.
        tree: CompetencyTree
            The tree for which we want to export scores.
        curriculum: Curriculum
            The curriculum for which we want to export recommendations.

        Returns
        -------
        list(list)
            Each element of the list is a list itself, representing a row.
            The first one contains the header of each column.
            The subsequent ones contain the list of user scores.

        """

        # We initialize exporters
        self._initialize_exporters(sessions, tree, curriculum)

        # We build rows for each session with a header row
        return [self._get_header_row()] + list(chain.from_iterable(
            [self._build_session_scores(session) for session in sessions]
        ))

    def _build_session_scores(self, session):
        """Build session scores for all trainees in provided session.

        Parameters
        ----------
        session: TrainingSession
            The session for which we want to export scores.

        Returns
        -------
        list(list)
            Return the session scores.

        """

        trainees = session.users.non_deleted().order_by('username')

        return [self._build_session_scores_for_trainee(session, trainee) for trainee in trainees]

    def _build_session_scores_for_trainee(self, session, trainee):
        """Build session scores for the provided trainee during a session.

        Parameters
        ----------
        session: TrainingSession
            The session used to extract trainee scores.
        trainee: MTPUser
            The trainee for which we want to extract scores.

        Returns
        -------
        list(int)
            This list contains the trainees scores.

        """

        trainee_id = trainee.id
        training = session.training
        business_unit_name = training.business_unit.name if training.business_unit else None
        session_date = formats.date_format(session.start_date, "SHORT_DATE_FORMAT")
        assessor = session.trainer.get_full_name()

        trainee_score = [
            # Each row starts with the business_unit name, session date,
            # session trainer and trainee's full name.
            business_unit_name, session_date, assessor, trainee.username, trainee.get_full_name(),

            # Then we have the trainee's global score
            self.certifications_exporter.get_global_score_for_trainee(session.id, trainee_id),

            # Then we have the trainee's certification level
            self.certifications_exporter.get_certification_for_trainee(session.id, trainee_id),
        ]

        # Then we have the trainee's score for each activity.
        trainee_score.extend(
            self.activities_exporter.get_activities_scores_for_trainee(session.id, trainee_id)
        )

        # Then we have the trainee's score for each competency, each of
        # which being followed by the trainee's score for each dimension
        # of said competency.
        trainee_score.extend(
            self.competencies_exporter.get_competencies_scores_for_trainee(session.id, trainee_id)
        )

        # Then we have the trainee's recommendation level for all lessons
        # of the curriculum.
        trainee_score.extend(
            self.recommendations_exporter.get_lessons_levels_for_trainee(session.id, trainee_id)
        )

        # The row is complete.
        return trainee_score

    @property
    def sessions_by_groups(self):
        """Filter the sessions by their job_profile and business_unit.

        Notes
        -----
        Before ``groupby`` sessions, we need to sort them, to have unique keys.
        But we can't sort with a dict. So we use the concatenation of business_unit id
        and job_profile id to create a unique identifier.

        Returns
        -------
        generator
            The sessions list grouped by their job_profile and business_unit.

        """

        sorted_sessions = sorted(
            self.sessions,
            key=lambda session: session.training.business_unit.id + session.training.job_profile.id
        )
        return groupby(
            sorted_sessions,
            lambda session: {
                'job_profile': session.training.job_profile,
                'business_unit': session.training.business_unit
            }
        )

    def _generate_xlsx(self, out_file):
        """Generate xlsx file.

        The generated file will have one sheet by job_profile to export.

        Parameters
        ----------
        out_file: str or io.BytesIO
            The path of file to create or a buffer.

        """

        workbook = Workbook(optimized_write=True)

        # We create one sheet by business_unit
        for groups, sessions in self.sessions_by_groups:

            # We select a range of 30 characters in business unit name
            # because sheet title cannot exceed 30 characters
            sheet = workbook.create_sheet(title=groups['business_unit'].name[13:43])
            tree = CompetencyTree.objects.get_for_job_profile(groups['job_profile'])
            curriculum = Curriculum.objects.get_for_groups(
                groups['job_profile'], groups['business_unit']
            )

            # Then, we append computed scores in each new sheet
            for row in self._build_sessions_scores(list(sessions), tree, curriculum):
                sheet.append(row)

        # If we have several worksheets, we create a global bulk
        if len(workbook.worksheets) > 1:
            sheet = workbook.create_sheet(title='Global Bulk', index=0)
            tree = CompetencyTree.objects.get_for_job_profile(
                self.sessions[0].training.job_profile
            )

            for row in self._build_sessions_scores(self.sessions, tree):
                sheet.append(row)

        workbook.save(out_file)

    def get_file_content(self):
        """Export sessions scores in a memory file.

        Returns
        -------
        Bytes
            The returned memory file.

        """

        output = io.BytesIO()
        self._generate_xlsx(output)

        return output.getvalue()

    def generate_file(self, file_path):
        """Generate a file with xlsx content.

        Parameters
        ----------
        file_path: str
            The file path where the file will be generated.

        """

        self._generate_xlsx(file_path)
