# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from collections import defaultdict
import logging

from course_blocks.api import get_course_blocks
from course_widget.grades import get_progress
from courseware import grades
from courseware.courses import get_course_by_id
from courseware.views.views import is_course_passed
from django.contrib.auth.models import User
from django.core.validators import MaxValueValidator
from django.db import models
from django.db.models import Sum, Count
from django.http import Http404
from django.utils import timezone
from django_countries.fields import CountryField
import lms.lib.comment_client as cc
from model_utils.models import TimeStampedModel
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from requests import ConnectionError
from student.models import CourseEnrollment
from track.backends.django import TrackingLog
from xmodule.modulestore.django import modulestore
from xmodule_django.models import CourseKeyField



IDLE_TIME = 900

ANALYTICS_ACCESS_GROUP = "Triboo Analytics Users"

log = logging.getLogger('lt_analytics')

DISPLAY_EXCLUDE = ['_state', 'modified']


class CourseStatus(object):
    not_started = 0
    in_progress = 1
    finished = 2
    failed = 3
    verbose_names = ['Not Started', 'In Progress', 'Successful', 'Unsuccessful']


def get_day_limits(day=None, offset=0):
    day = (day or timezone.now()) + timezone.timedelta(days=offset)
    day_start = day.replace(hour=0, minute=0, second=0, microsecond=0)
    day_end = day_start + timezone.timedelta(days=1)
    return day_start, day_end


def format_time_spent(seconds):
    m, s = divmod(seconds, 60)
    h, m = divmod(m, 60)
    return "%d:%02d:%02d" % (h, m, s)

def format_badges(badges_earned, badges_possible):
    return "{} / {}".format(badges_earned, badges_possible)


def get_badges(report_badges):
    badges = report_badges.split(" / ")
    if len(badges) == 2:
        return int(badges[0]), int(badges[1])
    return 0, 0


class UnicodeMixin(object):
    def __unicode__(self):
        result = {}
        for k, v in self.__dict__.iteritems():
            if k not in DISPLAY_EXCLUDE:
                result[k] = v.strftime('%Y-%m-%d %H:%S %Z') if isinstance(v, timezone.datetime) else v
        return unicode(result)



class ReportMixin(object):
    @classmethod
    def filter_by_day(cls, day=None, **kwargs):
        day_start, day_end = get_day_limits(day)
        return cls.objects.filter(created__gte=day_start,
                                  created__lt=day_end,
                                  **kwargs)

    @classmethod
    def get_by_day(cls, day=None, **kwargs):
        day_start, day_end = get_day_limits(day)
        try:
            return cls.objects.get(created__gte=day_start,
                                   created__lt=day_end,
                                   **kwargs)
        except cls.DoesNotExist:
            return None


def get_day_times_spent(day_start, day_end):
    tracking_logs = TrackingLog.objects.filter(
                        time__gte=day_start, time__lt=day_end).exclude(
                        user_id=None).only('event_type', 'time', 'user_id', 'agent')

    user_logs = defaultdict(list)

    for l in tracking_logs:
        user_logs[l.user_id].append(l)

    for user_id, logs in user_logs.iteritems():
        user_logs[user_id] = sorted(logs, key=lambda v: v.time)

    times_spent = {}
    for user_id, logs in user_logs.iteritems():
        user_times_spent = []
        if len(logs) >= 2:
            log_pairs = zip(logs[:-1], logs[1:])
            for log1, log2 in log_pairs:
                total_seconds = (log2.time - log1.time).total_seconds()
                time_spent = total_seconds if total_seconds < IDLE_TIME else IDLE_TIME
                user_times_spent.append((log1, time_spent))
            user_times_spent.append((logs[-1], IDLE_TIME))
        elif len(logs) == 1:
            user_times_spent.append((logs[0], IDLE_TIME))
        times_spent[user_id] = user_times_spent
    return times_spent


def generate_today_reports():
    LearnerVisitsDailyReport.generate_today_reports()
    LearnerCourseDailyReport.generate_today_reports()
    learner_course_reports = LearnerCourseDailyReport.filter_by_day().prefetch_related('user__profile')
    LearnerDailyReport.generate_today_reports(learner_course_reports)
    CourseDailyReport.generate_today_reports(learner_course_reports)
    MicrositeDailyReport.generate_today_reports(learner_course_reports)
    CountryDailyReport.generate_today_reports(learner_course_reports)


class ReportLog(UnicodeMixin, TimeStampedModel):
    class Meta(object):
        app_label = 'lt_analytics'
        get_latest_by = 'created'

    learner_visit = models.DateTimeField(default=None, null=True)
    learner_course = models.DateTimeField(default=None, null=True)
    learner = models.DateTimeField(default=None, null=True)
    course = models.DateTimeField(default=None, null=True)
    microsite = models.DateTimeField(default=None, null=True)
    country = models.DateTimeField(default=None, null=True)

    @classmethod
    def get_latest_dt(cls):
        try:
            report = cls.objects.filter(learner_visit__isnull=False,
                                        learner_course__isnull=False,
                                        learner__isnull=False,
                                        course__isnull=False,
                                        microsite__isnull=False,
                                        country__isnull=False).latest()
            return report.created
        except cls.DoesNotExist:
            return None

    @classmethod
    def update_or_create(cls, **kwargs):
        today_start, today_end = get_day_limits()
        cls.objects.update_or_create(created__gte=today_start,
                                     created__lt=today_end,
                                     defaults=kwargs)


class LearnerVisitsDailyReport(UnicodeMixin, ReportMixin, TimeStampedModel):
    class Meta(object):
        app_label = 'lt_analytics'
        get_latest_by = "created"
        unique_together = ('created', 'user', 'course_id', 'device')
        index_together = ['created', 'user', 'course_id', 'device']

    user = models.ForeignKey(User, null=False)
    course_id = CourseKeyField(max_length=255, null=True)
    org = models.CharField(max_length=255, db_index=True, null=True, default=None)
    device = models.CharField(max_length=255, null=False)
    time_spent = models.PositiveIntegerField(default=0)


    @classmethod
    def generate_day_reports(cls, day=None):
        previous_day_start, previous_day_end = get_day_limits(day=day, offset=-1)
        previous_day_times_spent = get_day_times_spent(previous_day_start, previous_day_end)
        for uid, one_user_times_spent in previous_day_times_spent.iteritems():
            cls.update_or_create(uid, one_user_times_spent, day)


    @classmethod
    def generate_today_reports(cls):
        cls.generate_day_reports()
        ReportLog.update_or_create(learner_visit=timezone.now())


    @classmethod
    def update_or_create(cls, user_id, one_user_times_spent, day=None):
        # group visit by course_id, device, accumulate time_spent
        reports = defaultdict(lambda: 0)
        for visit, time_spent in one_user_times_spent:
            reports[(visit.course_id, visit.device)] += time_spent

        today_start, today_end = get_day_limits(day)
        for (course_id, device), time_spent in reports.iteritems():
            org = course_id.org if course_id != CourseKeyField.Empty else None

            if day:
                cls.objects.update_or_create(user_id=user_id,
                                             course_id=course_id,
                                             org=org,
                                             device=device,
                                             created__gte=today_start,
                                             created__lt=today_end,
                                             created=day,
                                             defaults={'time_spent': int(time_spent)})

            else:
                cls.objects.update_or_create(user_id=user_id,
                                             course_id=course_id,
                                             org=org,
                                             device=device,
                                             created__gte=today_start,
                                             created__lt=today_end,
                                             defaults={'time_spent': int(time_spent)})


class LearnerCourseDailyReport(UnicodeMixin, ReportMixin, TimeStampedModel):
    class Meta(object):
        app_label = 'lt_analytics'
        get_latest_by = 'created'
        unique_together = ('user', 'course_id', 'created')
        index_together = ['user', 'course_id', 'created']

    user = models.ForeignKey(User, null=False)
    course_id = CourseKeyField(max_length=255, db_index=True, null=False)
    org = models.CharField(max_length=255, db_index=True, null=False)
    status = models.PositiveSmallIntegerField(help_text="not started: 0; in progress: 1; finished: 2; failed: 3; ",
                                              default=0)
    current_score = models.PositiveSmallIntegerField(default=0, validators=[MaxValueValidator(100)])
    badges = models.CharField(max_length=20, default="0 / 0")
    posts = models.IntegerField(default=0)
    progress = models.PositiveSmallIntegerField(default=0, validators=[MaxValueValidator(100)])
    total_time_spent = models.PositiveIntegerField(default=0)
    enrollment_date = models.DateTimeField(default=None, null=True, blank=True)
    completion_date = models.DateTimeField(default=None, null=True, blank=True)


    @classmethod
    def generate_today_reports(cls):
        enrollments = CourseEnrollment.objects.filter(is_active=True).prefetch_related('user')
        overviews = CourseOverview.objects.filter(start__lte=timezone.now()).only('id')
        course_ids = set([o.id for o in overviews])

        for enrollment in enrollments:
            if enrollment.course_id in course_ids:
                cls.update_or_create(enrollment)

        ReportLog.update_or_create(learner_course=timezone.now())

    @classmethod
    def update_or_create(cls, enrollment):
        """
        create today's summary. Call multiple times with the same parameters will only create one.
        Args:
            enrollment: CourseEnrollment object
            total_time_spent: total_time_spent calculated for this enrollment
        Returns: summary object.
        """
        course_id = enrollment.course_id
        user = enrollment.user
        today_start, today_end = get_day_limits()

        with modulestore().bulk_operations(course_id):
            if user.is_active:
                try:
                    course = get_course_by_id(course_id, None)
                except Http404:
                    return

                total_time_spent = (LearnerVisitsDailyReport.objects.filter(
                                        user=user, course_id=course_id).aggregate(
                                        Sum('time_spent')).get('time_spent__sum') or 0)

                course_structure = get_course_blocks(user, course.location)
                courseware_summary = grades.progress_summary(user, course, course_structure)
                grade_summary = grades.grade(user, course, course_structure=course_structure)
                if (not courseware_summary) or (not grade_summary):
                    log.warning('course: {} does not have progress info, skip.')
                    return
                progress = get_progress(user, course, courseware_summary, grade_summary, False)
                passed = is_course_passed(course, grade_summary)

                if progress['progress'] == 100:
                    status = CourseStatus.finished if passed else CourseStatus.failed
                else:
                    try:
                        last_report = cls.objects.exclude(created__gte=today_start, created__lt=today_end).filter(
                            course_id=course_id, user=user).latest()
                    except cls.DoesNotExist:
                        last_report = None

                    if last_report and last_report.status == CourseStatus.in_progress:
                        # if last time, it's already in progress, so keep it
                        status = last_report.status
                    else:
                        # if not, based on if he visit the course page to say if it started
                        if total_time_spent > 0 or progress['progress'] > 0:
                            status = CourseStatus.in_progress
                        else:
                            status = CourseStatus.not_started

                try:
                    cc_user = cc.User(id=user.id, course_id=course.id).to_dict()
                    posts = cc_user.get('comments_count', 0) + cc_user.get('threads_count', 0)
                except (cc.CommentClient500Error, cc.CommentClientRequestError, ConnectionError):
                    posts = 0

                cls.objects.update_or_create(
                    user=user, course_id=course_id,
                    created__gte=today_start, created__lt=today_end,
                    defaults={'org': course_id.org,
                              'progress': progress['progress'],
                              'badges': format_badges(progress['nb_trophies_earned'], progress['nb_trophies_possible']),
                              'current_score': progress['current_score'],
                              'enrollment_date': enrollment.created,
                              'completion_date': enrollment.completed,
                              'status': status,
                              'total_time_spent': total_time_spent,
                              'posts': posts})


class LearnerDailyReport(UnicodeMixin, ReportMixin, TimeStampedModel):
    class Meta(object):
        app_label = 'lt_analytics'
        get_latest_by = "created"
        unique_together = ('created', 'user', 'org')
        index_together = ['created', 'user', 'org']

    user = models.ForeignKey(User, null=False)
    org = models.CharField(max_length=255, db_index=True, null=False)
    enrollments = models.PositiveSmallIntegerField(default=0)
    average_final_score = models.PositiveSmallIntegerField(default=0)
    badges = models.CharField(max_length=20, default="0 / 0")
    posts = models.IntegerField(default=0)
    finished = models.SmallIntegerField(default=0, verbose_name=CourseStatus.verbose_names[CourseStatus.finished])
    failed = models.SmallIntegerField(default=0, verbose_name=CourseStatus.verbose_names[CourseStatus.failed])
    not_started = models.SmallIntegerField(default=0, verbose_name=CourseStatus.verbose_names[CourseStatus.not_started])
    in_progress = models.SmallIntegerField(default=0, verbose_name=CourseStatus.verbose_names[CourseStatus.in_progress])
    country = models.CharField(default='', max_length=255)
    total_time_spent = models.PositiveIntegerField(default=0)

    @classmethod
    def generate_today_reports(cls, learner_course_reports):
        reports_by_user_org = defaultdict(list)
        for report in learner_course_reports:
            reports_by_user_org[(report.user_id, report.org)].append(report)

        for (user_id, org), reports in reports_by_user_org.iteritems():
            cls.update_or_create(user_id, org, reports)

        ReportLog.update_or_create(learner=timezone.now())


    @classmethod
    def update_or_create(cls, user_id, org, learner_course_reports):
        enrollments = len(learner_course_reports)
        total_score = 0
        nb_completed_courses = 0
        posts = 0
        badges_earned = 0
        badges_possible = 0
        finished = 0
        failed = 0
        in_progress = 0
        not_started = 0
        for report in learner_course_reports:
            posts += report.posts
            earned, possible = get_badges(report.badges)
            badges_earned += earned
            badges_possible += possible
            if report.status == CourseStatus.finished:
                finished += 1
            elif report.status == CourseStatus.failed:
                failed += 1
            elif report.status == CourseStatus.in_progress:
                in_progress += 1
            elif report.status == CourseStatus.not_started:
                not_started += 1
            if report.status in [CourseStatus.finished, CourseStatus.failed]:
                total_score += report.current_score
                nb_completed_courses += 1
        average_final_score = 0
        if nb_completed_courses > 0:
            average_final_score = total_score / nb_completed_courses

        total_time_spent =  (LearnerVisitsDailyReport.objects.filter(user_id=user_id).aggregate(
                                Sum('time_spent')).get('time_spent__sum') or 0)

        today_start, today_end = get_day_limits()
        cls.objects.update_or_create(
            user_id=user_id,
            org=org,
            created__gte=today_start,
            created__lt=today_end,
            defaults={'enrollments': enrollments,
                      'average_final_score': average_final_score,
                      'total_time_spent': total_time_spent,
                      'posts': posts,
                      'badges': format_badges(badges_earned, badges_possible),
                      'finished': finished,
                      'failed': failed,
                      'in_progress': in_progress,
                      'not_started': not_started})


class CourseDailyReport(UnicodeMixin, ReportMixin, TimeStampedModel):
    class Meta(object):
        app_label = 'lt_analytics'
        get_latest_by = "created"
        unique_together = ('course_id', 'created')
        index_together = ['course_id', 'created']

    course_id = CourseKeyField(max_length=255, db_index=True, null=False)
    enrollments = models.PositiveIntegerField(default=0)
    average_final_score = models.PositiveSmallIntegerField(default=0)
    posts = models.IntegerField(default=0)
    finished = models.SmallIntegerField(default=0, verbose_name=CourseStatus.verbose_names[CourseStatus.finished])
    failed = models.SmallIntegerField(default=0, verbose_name=CourseStatus.verbose_names[CourseStatus.failed])
    in_progress = models.SmallIntegerField(default=0, verbose_name=CourseStatus.verbose_names[CourseStatus.in_progress])
    not_started = models.SmallIntegerField(default=0, verbose_name=CourseStatus.verbose_names[CourseStatus.not_started])
    average_complete_time = models.PositiveIntegerField(default=0)


    @classmethod
    def generate_today_reports(cls, learner_course_reports):
        reports_by_course = defaultdict(list)

        for report in learner_course_reports:
            reports_by_course[report.course_id].append(report)

        for course_id, reports in reports_by_course.iteritems():
            cls.update_or_create(course_id, reports)

        ReportLog.update_or_create(course=timezone.now())


    @classmethod
    def update_or_create(cls, course_id, learner_course_reports):
        total_score = 0
        nb_completed_courses = 0
        posts = 0
        finished = 0
        failed = 0
        in_progress = 0
        not_started = 0
        total_time = 0

        for report in learner_course_reports:
            posts += report.posts

            status = report.status
            if status == CourseStatus.finished:
                finished += 1
            elif status == CourseStatus.in_progress:
                in_progress += 1
            elif status == CourseStatus.not_started:
                not_started += 1
            elif status == CourseStatus.failed:
                failed += 1

            if status in [CourseStatus.finished, CourseStatus.failed]:
                total_score += report.current_score
                total_time += report.total_time_spent
                nb_completed_courses += 1

        average_final_score = average_complete_time = 0
        if nb_completed_courses > 0:
            average_final_score = total_score / nb_completed_courses
            average_complete_time = total_time / nb_completed_courses

        today_start, today_end = get_day_limits()
        cls.objects.update_or_create(
            course_id=course_id,
            created__gte=today_start,
            created__lt=today_end,
            defaults={'enrollments': len(learner_course_reports),
                      'average_final_score': average_final_score,
                      'posts': posts,
                      'finished': finished,
                      'failed': failed,
                      'in_progress': in_progress,
                      'not_started': not_started,
                      'average_complete_time': average_complete_time})


class MicrositeDailyReport(UnicodeMixin, ReportMixin, TimeStampedModel):
    class Meta:
        app_label = 'lt_analytics'
        get_latest_by = "created"
        unique_together = ('created', 'org')
        index_together = ['created', 'org']

    org = models.CharField(max_length=255, null=False)
    users = models.PositiveIntegerField(default=0)
    courses = models.PositiveIntegerField(default=0)
    finished = models.PositiveIntegerField(default=0)
    unique_visitors = models.PositiveIntegerField(default=0)
    average_time_spent = models.PositiveIntegerField(default=0)
    total_time_spent_on_mobile = models.PositiveIntegerField(default=0)
    total_time_spent_on_desktop = models.PositiveIntegerField(default=0)


    @classmethod
    def generate_today_reports(cls, learner_course_reports):
        reports_by_org = defaultdict(list)
        for report in learner_course_reports:
            reports_by_org[report.org].append(report)

        for org, reports in reports_by_org.iteritems():
            cls.update_or_create(org, reports)

        ReportLog.update_or_create(microsite=timezone.now())

    @classmethod
    def update_or_create(cls, org, learner_course_reports):
        users = set()
        courses = set()
        finished = 0
        total_time_spent = 0
        time_count = 0
        for report in learner_course_reports:
            users.add(report.user_id)
            courses.add(unicode(report.course_id))
            if report.status == CourseStatus.finished:
                finished += 1
                total_time_spent += report.total_time_spent
                time_count += 1

        average_time_spent = total_time_spent / time_count if time_count else 0

        total_time_spent_on_mobile = 0
        total_time_spent_on_desktop = 0
        for course_id in courses:
            course_key = CourseKey.from_string(course_id)
            total_time_spent_on_mobile += (LearnerVisitsDailyReport.objects.filter(
                                            course_id=course_key, device="mobile").aggregate(
                                            Sum('time_spent')).get('time_spent__sum') or 0)
            total_time_spent_on_desktop += (LearnerVisitsDailyReport.objects.filter(
                                            course_id=course_key, device="desktop").aggregate(
                                            Sum('time_spent')).get('time_spent__sum') or 0)

        unique_visitors = (LearnerVisitsDailyReport.filter_by_day(org=org).aggregate(
                            Count('user_id', distinct=True)).get('user_id__count') or 0)

        today_start, today_end = get_day_limits()
        cls.objects.update_or_create(
            org=org,
            created__gte=today_start,
            created__lt=today_end,
            defaults={'users': len(users),
                      'courses': len(courses),
                      'finished': finished,
                      'unique_visitors': unique_visitors,
                      'average_time_spent': average_time_spent,
                      'total_time_spent_on_mobile': total_time_spent_on_mobile,
                      'total_time_spent_on_desktop': total_time_spent_on_desktop})


    @classmethod
    def update_or_create_unique_visitors(cls, day, org):
        day_start, day_end = get_day_limits(day)
        unique_visitors = (LearnerVisitsDailyReport.filter_by_day(day=day, org=org).aggregate(
                            Count('user_id', distinct=True)).get('user_id__count') or 0)
        print "day=%s, org=%s" % (day, org)
        cls.objects.update_or_create(
            org=org,
            created__gte=day_start,
            created__lt=day_end,
            defaults={'created': day, 'unique_visitors': unique_visitors})


    @classmethod
    def get_unique_visitors_csv_data(cls, org):
        unique_visitors_csv_data = ""
        unique_visitors = cls.objects.filter(org=org).values('created', 'unique_visitors').order_by('created')
        for uv in unique_visitors:
            unique_visitors_csv_data += "%s,%d\\n" % (uv['created'].strftime('%d-%m-%Y'), uv['unique_visitors'])
        return unique_visitors_csv_data


class CountryDailyReport(UnicodeMixin, ReportMixin, TimeStampedModel):
    class Meta:
        app_label = 'lt_analytics'
        get_latest_by = "created"
        unique_together = ('created', 'org', 'country')
        index_together = ['created', 'org']

    org = models.CharField(max_length=255, null=False)
    country = CountryField(null=True)
    nb_users = models.PositiveIntegerField(default=0)


    @classmethod
    def generate_today_reports(cls, learner_course_reports):
        reports_by_org = defaultdict(list)
        for report in learner_course_reports:
            reports_by_org[report.org].append(report)

        for org, reports in reports_by_org.iteritems():
            cls.update_or_create(org, reports)

        ReportLog.update_or_create(country=timezone.now())


    @classmethod
    def update_or_create(cls, org, learner_course_reports):
        users_by_country = defaultdict(int)
        users = []
        for report in learner_course_reports:
            if report.user.id not in users:
                users.append(report.user.id)
                users_by_country[report.user.profile.country] += 1

        for country, nb_users in users_by_country.iteritems():
            today_start, today_end = get_day_limits()
            cls.objects.update_or_create(
                org=org,
                country=country,
                created__gte=today_start,
                created__lt=today_end,
                defaults={'nb_users': nb_users})
