#       Licensed to the Apache Software Foundation (ASF) under one
#       or more contributor license agreements.  See the NOTICE file
#       distributed with this work for additional information
#       regarding copyright ownership.  The ASF licenses this file
#       to you under the Apache License, Version 2.0 (the
#       "License"); you may not use this file except in compliance
#       with the License.  You may obtain a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#       Unless required by applicable law or agreed to in writing,
#       software distributed under the License is distributed on an
#       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
#       KIND, either express or implied.  See the License for the
#       specific language governing permissions and limitations
#       under the License.


import six
from datetime import datetime
import typing
from tg import config
from paste.deploy.converters import asbool

from ming import schema as S
from ming.odm import Mapper
from ming.odm import FieldProperty
from ming.odm.declarative import MappedClass
from datetime import timedelta
import difflib

from allura.model.session import main_orm_session
from allura.lib import helpers as h
from functools import reduce

if typing.TYPE_CHECKING:
    from ming.odm.mapper import Query


class Stats(MappedClass):

    class __mongometa__:
        name = 'basestats'
        session = main_orm_session
        unique_indexes = ['_id']

    query: 'Query[Stats]'

    _id = FieldProperty(S.ObjectId)

    visible = FieldProperty(bool, if_missing=True)
    registration_date = FieldProperty(datetime)
    general = FieldProperty([dict(
        category=S.ObjectId,
        messages=[dict(
            messagetype=str,
            created=int,
            modified=int)],
        tickets=dict(
            solved=int,
            assigned=int,
            revoked=int,
            totsolvingtime=int),
        commits=[dict(
            lines=int,
            number=int,
            language=S.ObjectId)])])

    lastmonth = FieldProperty(dict(
        messages=[dict(
            datetime=datetime,
            created=bool,
            categories=[S.ObjectId],
            messagetype=str)],
        assignedtickets=[dict(
            datetime=datetime,
            categories=[S.ObjectId])],
        revokedtickets=[dict(
            datetime=datetime,
            categories=[S.ObjectId])],
        solvedtickets=[dict(
            datetime=datetime,
            categories=[S.ObjectId],
            solvingtime=int)],
        commits=[dict(
            datetime=datetime,
            categories=[S.ObjectId],
            programming_languages=[S.ObjectId],
            lines=int)]))

    @property
    def start_date(self):
        """Date from which stats should be calculated.

        The user may have registered before stats were collected,
        making calculations based on registration date unfair."""
        min_date = config.get('userstats.start_date', '0001-1-1')
        return max(datetime.strptime(min_date, '%Y-%m-%d'), self.registration_date)

    def getCommits(self, category=None):
        i = getElementIndex(self.general, category=category)
        if i is None:
            return dict(number=0, lines=0)
        cat = self.general[i]
        j = getElementIndex(cat.commits, language=None)
        if j is None:
            return dict(number=0, lines=0)
        return dict(
            number=cat.commits[j]['number'],
            lines=cat.commits[j]['lines'])

    def getArtifacts(self, category=None, art_type=None):
        i = getElementIndex(self.general, category=category)
        if i is None:
            return dict(created=0, modified=0)
        cat = self.general[i]
        j = getElementIndex(cat.messages, messagetype=art_type)
        if j is None:
            return dict(created=0, modified=0)
        return dict(created=cat.messages[j].created, modified=cat.messages[j].modified)

    def getTickets(self, category=None):
        i = getElementIndex(self.general, category=category)
        if i is None:
            return dict(
                assigned=0,
                solved=0,
                revoked=0,
                averagesolvingtime=None)
        if self.general[i].tickets.solved > 0:
            tot = self.general[i].tickets.totsolvingtime
            number = self.general[i].tickets.solved
            average = tot / number
        else:
            average = None
        return dict(
            assigned=self.general[i].tickets.assigned,
            solved=self.general[i].tickets.solved,
            revoked=self.general[i].tickets.revoked,
            averagesolvingtime=_convertTimeDiff(average))

    def getCommitsByCategory(self):
        from allura.model.project import TroveCategory

        by_cat = {}
        for entry in self.general:
            cat = entry.category
            i = getElementIndex(entry.commits, language=None)
            if i is None:
                n, lines = 0, 0
            else:
                n, lines = entry.commits[i].number, entry.commits[i].lines
            if cat is not None:
                cat = TroveCategory.query.get(_id=cat)
            by_cat[cat] = dict(number=n, lines=lines)
        return by_cat

    def getArtifactsByCategory(self, detailed=False):
        from allura.model.project import TroveCategory

        by_cat = {}
        for entry in self.general:
            cat = entry.category
            if cat is not None:
                cat = TroveCategory.query.get(_id=cat)
            if detailed:
                by_cat[cat] = entry.messages
            else:
                i = getElementIndex(entry.messages, messagetype=None)
                if i is not None:
                    by_cat[cat] = entry.messages[i]
                else:
                    by_cat[cat] = dict(created=0, modified=0)
        return by_cat

    def getArtifactsByType(self, category=None):
        i = getElementIndex(self.general, category=category)
        if i is None:
            return {}
        entry = self.general[i].messages
        by_type = {el.messagetype: dict(created=el.created,
                                        modified=el.modified)
                   for el in entry}
        return by_type

    def getTicketsByCategory(self):
        from allura.model.project import TroveCategory

        by_cat = {}
        for entry in self.general:
            cat = entry.category
            if cat is not None:
                cat = TroveCategory.query.get(_id=cat)
            a, s = entry.tickets.assigned, entry.tickets.solved
            r, time = entry.tickets.solved, entry.tickets.totsolvingtime
            if s:
                average = time / s
            else:
                average = None
            by_cat[cat] = dict(
                assigned=a,
                solved=s,
                revoked=r,
                averagesolvingtime=_convertTimeDiff(average))
        return by_cat

    def getLastMonthCommits(self, category=None):
        self.checkOldArtifacts()
        lineslist = [el.lines for el in self.lastmonth.commits
                     if category in el.categories + [None]]
        return dict(number=len(lineslist), lines=sum(lineslist))

    def getLastMonthCommitsByCategory(self):
        from allura.model.project import TroveCategory

        self.checkOldArtifacts()
        seen = set()
        catlist = [el.category for el in self.general
                   if el.category not in seen and not seen.add(el.category)]

        by_cat = {}
        for cat in catlist:
            lineslist = [el.lines for el in self.lastmonth.commits
                         if cat in el.categories + [None]]
            n = len(lineslist)
            lines = sum(lineslist)
            if cat is not None:
                cat = TroveCategory.query.get(_id=cat)
            by_cat[cat] = dict(number=n, lines=lines)
        return by_cat

    def getLastMonthArtifacts(self, category=None, art_type=None):
        self.checkOldArtifacts()
        cre, mod = reduce(
            addtuple,
            [(int(el.created), 1 - int(el.created))
                for el in self.lastmonth.messages
                if (category is None or category in el.categories) and
                (el.messagetype == art_type or art_type is None)],
            (0, 0))
        return dict(created=cre, modified=mod)

    def getLastMonthArtifactsByType(self, category=None):
        self.checkOldArtifacts()
        seen = set()
        types = [el.messagetype for el in self.lastmonth.messages
                 if el.messagetype not in seen and not seen.add(el.messagetype)]

        by_type = {}
        for t in types:
            cre, mod = reduce(
                addtuple,
                [(int(el.created), 1 - int(el.created))
                 for el in self.lastmonth.messages
                 if el.messagetype == t and
                 category in [None] + el.categories],
                (0, 0))
            by_type[t] = dict(created=cre, modified=mod)
        return by_type

    def getLastMonthTickets(self, category=None):
        from allura.model.project import TroveCategory

        self.checkOldArtifacts()
        a = len([el for el in self.lastmonth.assignedtickets
                 if category in el.categories + [None]])
        r = len([el for el in self.lastmonth.revokedtickets
                 if category in el.categories + [None]])
        s, time = reduce(
            addtuple,
            [(1, el.solvingtime)
             for el in self.lastmonth.solvedtickets
             if category in el.categories + [None]],
            (0, 0))
        if category is not None:
            category = TroveCategory.query.get(_id=category)
        if s > 0:
            time = time / s
        else:
            time = None
        return dict(
            assigned=a,
            revoked=r,
            solved=s,
            averagesolvingtime=_convertTimeDiff(time))

    def getLastMonthTicketsByCategory(self):
        from allura.model.project import TroveCategory

        self.checkOldArtifacts()
        seen = set()
        catlist = [el.category for el in self.general
                   if el.category not in seen and not seen.add(el.category)]
        by_cat = {}
        for cat in catlist:
            a = len([el for el in self.lastmonth.assignedtickets
                     if cat in el.categories + [None]])
            r = len([el for el in self.lastmonth.revokedtickets
                     if cat in el.categories + [None]])
            s, time = reduce(addtuple, [(1, el.solvingtime)
                                        for el in self.lastmonth.solvedtickets
                                        if cat in el.categories + [None]], (0, 0))
            if cat is not None:
                cat = TroveCategory.query.get(_id=cat)
            if s > 0:
                time = time / s
            else:
                time = None
            by_cat[cat] = dict(
                assigned=a,
                revoked=r,
                solved=s,
                averagesolvingtime=_convertTimeDiff(time))
        return by_cat

    def checkOldArtifacts(self):
        now = datetime.utcnow()
        for m in self.lastmonth.messages:
            if now - m.datetime > timedelta(30):
                self.lastmonth.messages.remove(m)
        for t in self.lastmonth.assignedtickets:
            if now - t.datetime > timedelta(30):
                self.lastmonth.assignedtickets.remove(t)
        for t in self.lastmonth.revokedtickets:
            if now - t.datetime > timedelta(30):
                self.lastmonth.revokedtickets.remove(t)
        for t in self.lastmonth.solvedtickets:
            if now - t.datetime > timedelta(30):
                self.lastmonth.solvedtickets.remove(t)
        for c in self.lastmonth.commits:
            if now - c.datetime > timedelta(30):
                self.lastmonth.commits.remove(c)

    def addNewArtifact(self, art_type, art_datetime, project):
        self._updateArtifactsStats(art_type, art_datetime, project, "created")

    def addModifiedArtifact(self, art_type, art_datetime, project):
        self._updateArtifactsStats(art_type, art_datetime, project, "modified")

    def addAssignedTicket(self, ticket_datetime, project):
        topics = [t for t in project.trove_topic if t]
        self._updateTicketsStats(topics, 'assigned')
        self.lastmonth.assignedtickets.append(
            dict(datetime=ticket_datetime, categories=topics))

    def addRevokedTicket(self, ticket_datetime, project):
        topics = [t for t in project.trove_topic if t]
        self._updateTicketsStats(topics, 'revoked')
        self.lastmonth.revokedtickets.append(
            dict(datetime=ticket_datetime, categories=topics))
        self.checkOldArtifacts()

    def addClosedTicket(self, open_datetime, close_datetime, project):
        topics = [t for t in project.trove_topic if t]
        s_time = int((close_datetime - open_datetime).total_seconds())
        self._updateTicketsStats(topics, 'solved', s_time=s_time)
        self.lastmonth.solvedtickets.append(dict(
            datetime=close_datetime,
            categories=topics,
            solvingtime=s_time))
        self.checkOldArtifacts()

    def addCommit(self, newcommit, commit_datetime, project):
        def _computeLines(newblob, oldblob=None):
            if oldblob:
                listold = list(oldblob)
            else:
                listold = []
            if newblob:
                listnew = list(newblob)
            else:
                listnew = []

            if oldblob is None:
                lines = len(listnew)
            elif newblob and newblob.has_html_view:
                # py2 unified_diff can handle some unicode but not consistently, so best to do ensure_str (can drop it on py3)
                diff = difflib.unified_diff(
                    [six.ensure_str(h.really_unicode(line)) for line in listold],
                    [six.ensure_str(h.really_unicode(line)) for line in listnew],
                    six.ensure_str('old' + oldblob.path()),
                    six.ensure_str('new' + newblob.path()))
                lines = len(
                    [l for l in diff if len(l) > 0 and l[0] == '+']) - 1
            else:
                lines = 0
            return lines

        def _addCommitData(stats, topics, languages, lines):
            lt = topics + [None]
            ll = languages + [None]
            for t in lt:
                i = getElementIndex(stats.general, category=t)
                if i is None:
                    newstats = dict(
                        category=t,
                        commits=[],
                        messages=[],
                        tickets=dict(
                            assigned=0,
                            solved=0,
                            revoked=0,
                            totsolvingtime=0))
                    stats.general.append(newstats)
                    i = getElementIndex(stats.general, category=t)
                for lang in ll:
                    j = getElementIndex(
                        stats.general[i]['commits'], language=lang)
                    if j is None:
                        stats.general[i]['commits'].append(dict(
                            language=lang, lines=lines, number=1))
                    else:
                        stats.general[i]['commits'][j].lines += lines
                        stats.general[i]['commits'][j].number += 1

        topics = [t for t in project.trove_topic if t]
        languages = [l for l in project.trove_language if l]

        d = newcommit.diffs
        if len(newcommit.parent_ids) > 0:
            oldcommit = newcommit.repo.commit(newcommit.parent_ids[0])

        totlines = 0
        if asbool(config.get('userstats.count_lines_of_code', True)):
            for changed in d.changed:
                newblob = newcommit.tree.get_blob_by_path(changed)
                oldblob = oldcommit.tree.get_blob_by_path(changed)
                totlines += _computeLines(newblob, oldblob)

            for copied in d.copied:
                newblob = newcommit.tree.get_blob_by_path(copied['new'])
                oldblob = oldcommit.tree.get_blob_by_path(copied['old'])
                totlines += _computeLines(newblob, oldblob)

            for added in d.added:
                newblob = newcommit.tree.get_blob_by_path(added)
                totlines += _computeLines(newblob)

        _addCommitData(self, topics, languages, totlines)

        self.lastmonth.commits.append(dict(
            datetime=commit_datetime,
            categories=topics,
            programming_languages=languages,
            lines=totlines))
        self.checkOldArtifacts()

    def _updateArtifactsStats(self, art_type, art_datetime, project, action):
        if action not in ['created', 'modified']:
            return
        topics = [t for t in project.trove_topic if t]
        lt = [None] + topics
        for mtype in [None, art_type]:
            for t in lt:
                i = getElementIndex(self.general, category=t)
                if i is None:
                    msg = dict(
                        category=t,
                        commits=[],
                        tickets=dict(
                            solved=0,
                            assigned=0,
                            revoked=0,
                            totsolvingtime=0),
                        messages=[])
                    self.general.append(msg)
                    i = getElementIndex(self.general, category=t)
                j = getElementIndex(
                    self.general[i]['messages'], messagetype=mtype)
                if j is None:
                    entry = dict(messagetype=mtype, created=0, modified=0)
                    entry[action] += 1
                    self.general[i]['messages'].append(entry)
                else:
                    self.general[i]['messages'][j][action] += 1

        self.lastmonth.messages.append(dict(
            datetime=art_datetime,
            created=(action == 'created'),
            categories=topics,
            messagetype=art_type))
        self.checkOldArtifacts()

    def _updateTicketsStats(self, topics, action, s_time=None):
        if action not in ['solved', 'assigned', 'revoked']:
            return
        lt = topics + [None]
        for t in lt:
            i = getElementIndex(self.general, category=t)
            if i is None:
                stats = dict(
                    category=t,
                    commits=[],
                    tickets=dict(
                        solved=0,
                        assigned=0,
                        revoked=0,
                        totsolvingtime=0),
                    messages=[])
                self.general.append(stats)
                i = getElementIndex(self.general, category=t)
            self.general[i]['tickets'][action] += 1
            if action == 'solved':
                self.general[i]['tickets']['totsolvingtime'] += s_time


def getElementIndex(el_list, **kw):
    for i in range(len(el_list)):
        for k, v in kw.items():
            if el_list[i].get(k) != v:
                break
        else:
            return i
    return None


def addtuple(l1, l2):
    a, b = l1
    x, y = l2
    return (a + x, b + y)


def _convertTimeDiff(int_seconds):
    if int_seconds is None:
        return None
    diff = timedelta(seconds=int_seconds)
    days, seconds = diff.days, diff.seconds
    hours = seconds // 3600
    seconds = seconds % 3600
    minutes = seconds // 60
    seconds = seconds % 60
    return dict(
        days=days,
        hours=hours,
        minutes=minutes,
        seconds=seconds)


Mapper.compile_all()
