# -*- coding: utf-8 -*-
#
# Copyright © 2012 - 2020 Michal Čihař <michal@cihar.com>
#
# This file is part of Weblate <https://weblate.org/>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
from __future__ import unicode_literals

import six.moves
from django.conf import settings
from django.db import models, transaction
from django.db.models import Count, Q
from django.utils import timezone
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
from six import python_2_unicode_compatible

from weblate.lang.models import Language
from weblate.trans.mixins import UserDisplayMixin
from weblate.trans.models.alert import ALERTS
from weblate.trans.models.project import Project
from weblate.utils.fields import JSONField


class ChangeQuerySet(models.QuerySet):
    # pylint: disable=no-init

    def content(self, prefetch=False):
        """Return queryset with content changes."""
        base = self
        if prefetch:
            base = base.prefetch()
        return base.filter(action__in=Change.ACTIONS_CONTENT)

    @staticmethod
    def count_stats(days, step, dtstart, base):
        """Count number of changes in given dataset and period grouped by
        step days.
        """

        # Count number of changes
        result = []
        for _unused in six.moves.range(0, days, step):
            # Calculate interval
            int_start = dtstart
            int_end = int_start + timezone.timedelta(days=step)

            # Count changes
            int_base = base.filter(timestamp__range=(int_start, int_end))
            count = int_base.aggregate(Count('id'))

            # Append to result
            result.append((int_start, count['id__count']))

            # Advance to next interval
            dtstart = int_end

        return result

    def base_stats(
        self,
        days,
        step,
        project=None,
        component=None,
        translation=None,
        language=None,
        user=None,
    ):
        """Core of daily/weekly/monthly stats calculation."""

        # Get range (actually start)
        dtstart = timezone.now() - timezone.timedelta(days=days + 1)

        # Base for filtering
        base = self.all()

        # Filter by translation/project
        if translation is not None:
            base = base.filter(translation=translation)
        elif component is not None:
            base = base.filter(translation__component=component)
        elif project is not None:
            base = base.filter(translation__component__project=project)

        # Filter by language
        if language is not None:
            base = base.filter(translation__language=language)

        # Filter by language
        if user is not None:
            base = base.filter(user=user)

        return self.count_stats(days, step, dtstart, base)

    def prefetch(self):
        """Fetch related fields in a big chungs to avoid loading them
        individually.
        """
        return self.select_related(
            'user',
            'translation',
            'component',
            'project',
            'unit',
            'dictionary',
            'translation__language',
            'translation__component',
            'translation__component__project',
            'unit__translation',
            'unit__translation__language',
            'unit__translation__plural',
            'unit__translation__component',
            'unit__translation__component__project',
            'component__project',
        )

    def last_changes(self, user):
        """Prefilter Changes by ACL for users and fetches related fields
        for last changes display.
        """
        return (
            self.prefetch()
            .filter(
                Q(component__project__in=user.allowed_projects)
                | Q(dictionary__project__in=user.allowed_projects)
            )
            .order()
        )

    def authors_list(self, date_range=None):
        """Return list of authors."""
        authors = self.content()
        if date_range is not None:
            authors = authors.filter(timestamp__range=date_range)
        return authors.values_list('author__email', 'author__full_name')

    def order(self):
        return self.order_by('-timestamp')


class ChangeManager(models.Manager):
    def create(self, user=None, **kwargs):
        """Wrapper to avoid using anonymous user as change owner"""
        if user is not None and not user.is_authenticated:
            user = None
        return super(ChangeManager, self).create(user=user, **kwargs)


@python_2_unicode_compatible
class Change(models.Model, UserDisplayMixin):
    ACTION_UPDATE = 0
    ACTION_COMPLETE = 1
    ACTION_CHANGE = 2
    ACTION_COMMENT = 3
    ACTION_SUGGESTION = 4
    ACTION_NEW = 5
    ACTION_AUTO = 6
    ACTION_ACCEPT = 7
    ACTION_REVERT = 8
    ACTION_UPLOAD = 9
    ACTION_DICTIONARY_NEW = 10
    ACTION_DICTIONARY_EDIT = 11
    ACTION_DICTIONARY_UPLOAD = 12
    ACTION_NEW_SOURCE = 13
    ACTION_LOCK = 14
    ACTION_UNLOCK = 15
    ACTION_DUPLICATE_STRING = 16
    ACTION_COMMIT = 17
    ACTION_PUSH = 18
    ACTION_RESET = 19
    ACTION_MERGE = 20
    ACTION_REBASE = 21
    ACTION_FAILED_MERGE = 22
    ACTION_FAILED_REBASE = 23
    ACTION_PARSE_ERROR = 24
    ACTION_REMOVE_TRANSLATION = 25
    ACTION_SUGGESTION_DELETE = 26
    ACTION_REPLACE = 27
    ACTION_FAILED_PUSH = 28
    ACTION_SUGGESTION_CLEANUP = 29
    ACTION_SOURCE_CHANGE = 30
    ACTION_NEW_UNIT = 31
    ACTION_MASS_STATE = 32
    ACTION_ACCESS_EDIT = 33
    ACTION_ADD_USER = 34
    ACTION_REMOVE_USER = 35
    ACTION_APPROVE = 36
    ACTION_MARKED_EDIT = 37
    ACTION_REMOVE_COMPONENT = 38
    ACTION_REMOVE_PROJECT = 39
    ACTION_DUPLICATE_LANGUAGE = 40
    ACTION_RENAME_PROJECT = 41
    ACTION_RENAME_COMPONENT = 42
    ACTION_MOVE_COMPONENT = 43
    ACTION_NEW_STRING = 44
    ACTION_NEW_CONTRIBUTOR = 45
    ACTION_MESSAGE = 46
    ACTION_ALERT = 47
    ACTION_ADDED_LANGUAGE = 48
    ACTION_REQUESTED_LANGUAGE = 49
    ACTION_CREATE_PROJECT = 50
    ACTION_CREATE_COMPONENT = 51
    ACTION_INVITE_USER = 52
    ACTION_HOOK = 53

    ACTION_CHOICES = (
        (ACTION_UPDATE, ugettext_lazy('Resource update')),
        (ACTION_COMPLETE, ugettext_lazy('Translation completed')),
        (ACTION_CHANGE, ugettext_lazy('Translation changed')),
        (ACTION_NEW, ugettext_lazy('New translation')),
        (ACTION_COMMENT, ugettext_lazy('Comment added')),
        (ACTION_SUGGESTION, ugettext_lazy('Suggestion added')),
        (ACTION_AUTO, ugettext_lazy('Automatic translation')),
        (ACTION_ACCEPT, ugettext_lazy('Suggestion accepted')),
        (ACTION_REVERT, ugettext_lazy('Translation reverted')),
        (ACTION_UPLOAD, ugettext_lazy('Translation uploaded')),
        (ACTION_DICTIONARY_NEW, ugettext_lazy('Glossary added')),
        (ACTION_DICTIONARY_EDIT, ugettext_lazy('Glossary updated')),
        (ACTION_DICTIONARY_UPLOAD, ugettext_lazy('Glossary uploaded')),
        (ACTION_NEW_SOURCE, ugettext_lazy('New source string')),
        (ACTION_LOCK, ugettext_lazy('Component locked')),
        (ACTION_UNLOCK, ugettext_lazy('Component unlocked')),
        (ACTION_DUPLICATE_STRING, ugettext_lazy('Found duplicated string')),
        (ACTION_COMMIT, ugettext_lazy('Committed changes')),
        (ACTION_PUSH, ugettext_lazy('Pushed changes')),
        (ACTION_RESET, ugettext_lazy('Reset repository')),
        (ACTION_MERGE, ugettext_lazy('Merged repository')),
        (ACTION_REBASE, ugettext_lazy('Rebased repository')),
        (ACTION_FAILED_MERGE, ugettext_lazy('Failed merge on repository')),
        (ACTION_FAILED_REBASE, ugettext_lazy('Failed rebase on repository')),
        (ACTION_FAILED_PUSH, ugettext_lazy('Failed push on repository')),
        (ACTION_PARSE_ERROR, ugettext_lazy('Parse error')),
        (ACTION_REMOVE_TRANSLATION, ugettext_lazy('Removed translation')),
        (ACTION_SUGGESTION_DELETE, ugettext_lazy('Suggestion removed')),
        (ACTION_REPLACE, ugettext_lazy('Search and replace')),
        (ACTION_SUGGESTION_CLEANUP, ugettext_lazy('Suggestion removed during cleanup')),
        (ACTION_SOURCE_CHANGE, ugettext_lazy('Source string changed')),
        (ACTION_NEW_UNIT, ugettext_lazy('New string added')),
        (ACTION_MASS_STATE, ugettext_lazy('Bulk status change')),
        (ACTION_ACCESS_EDIT, ugettext_lazy('Changed visibility')),
        (ACTION_ADD_USER, ugettext_lazy('Added user')),
        (ACTION_REMOVE_USER, ugettext_lazy('Removed user')),
        (ACTION_APPROVE, ugettext_lazy('Translation approved')),
        (ACTION_MARKED_EDIT, ugettext_lazy('Marked for edit')),
        (ACTION_REMOVE_COMPONENT, ugettext_lazy('Removed component')),
        (ACTION_REMOVE_PROJECT, ugettext_lazy('Removed project')),
        (ACTION_DUPLICATE_LANGUAGE, ugettext_lazy('Found duplicated language')),
        (ACTION_RENAME_PROJECT, ugettext_lazy('Renamed project')),
        (ACTION_RENAME_COMPONENT, ugettext_lazy('Renamed component')),
        (ACTION_MOVE_COMPONENT, ugettext_lazy('Moved component')),
        (ACTION_NEW_STRING, ugettext_lazy('New string to translate')),
        (ACTION_NEW_CONTRIBUTOR, ugettext_lazy('New contributor')),
        (ACTION_MESSAGE, ugettext_lazy('New whiteboard message')),
        (ACTION_ALERT, ugettext_lazy('New alert')),
        (ACTION_ADDED_LANGUAGE, ugettext_lazy('Added new language')),
        (ACTION_REQUESTED_LANGUAGE, ugettext_lazy('Requested new language')),
        (ACTION_CREATE_PROJECT, ugettext_lazy('Created project')),
        (ACTION_CREATE_COMPONENT, ugettext_lazy('Created component')),
        (ACTION_INVITE_USER, ugettext_lazy('Invited user')),
        (ACTION_HOOK, ugettext_lazy('Received repository notification')),
    )

    # Actions which can be reverted
    ACTIONS_REVERTABLE = {
        ACTION_ACCEPT,
        ACTION_REVERT,
        ACTION_CHANGE,
        ACTION_UPLOAD,
        ACTION_NEW,
        ACTION_REPLACE,
        ACTION_AUTO,
    }

    # Content changes considered when looking for last author
    ACTIONS_CONTENT = {
        ACTION_CHANGE,
        ACTION_NEW,
        ACTION_AUTO,
        ACTION_ACCEPT,
        ACTION_REVERT,
        ACTION_UPLOAD,
        ACTION_REPLACE,
        ACTION_MASS_STATE,
        ACTION_APPROVE,
        ACTION_MARKED_EDIT,
    }

    # Actions considered as being translated in consistency check
    ACTIONS_TRANSLATED = {
        ACTION_CHANGE,
        ACTION_NEW,
        ACTION_AUTO,
        ACTION_ACCEPT,
        ACTION_REVERT,
        ACTION_UPLOAD,
        ACTION_REPLACE,
        ACTION_APPROVE,
    }

    # Actions shown on the repository management page
    ACTIONS_REPOSITORY = {
        ACTION_COMMIT,
        ACTION_PUSH,
        ACTION_RESET,
        ACTION_MERGE,
        ACTION_REBASE,
        ACTION_FAILED_MERGE,
        ACTION_FAILED_REBASE,
        ACTION_FAILED_PUSH,
        ACTION_LOCK,
        ACTION_UNLOCK,
        ACTION_DUPLICATE_LANGUAGE,
    }

    # Actions where target is rendered as translation string
    ACTIONS_SHOW_CONTENT = {
        ACTION_SUGGESTION,
        ACTION_SUGGESTION_DELETE,
        ACTION_SUGGESTION_CLEANUP,
        ACTION_NEW_UNIT,
        ACTION_DICTIONARY_NEW,
        ACTION_DICTIONARY_EDIT,
    }

    # Actions indicating a repository merge failure
    ACTIONS_MERGE_FAILURE = {
        ACTION_FAILED_MERGE,
        ACTION_FAILED_REBASE,
        ACTION_FAILED_PUSH,
    }

    unit = models.ForeignKey('Unit', null=True, on_delete=models.deletion.CASCADE)
    project = models.ForeignKey('Project', null=True, on_delete=models.deletion.CASCADE)
    component = models.ForeignKey(
        'Component', null=True, on_delete=models.deletion.CASCADE
    )
    translation = models.ForeignKey(
        'Translation', null=True, on_delete=models.deletion.CASCADE
    )
    dictionary = models.ForeignKey(
        'Dictionary', null=True, on_delete=models.deletion.CASCADE
    )
    comment = models.ForeignKey(
        'Comment', null=True, on_delete=models.deletion.SET_NULL
    )
    suggestion = models.ForeignKey(
        'Suggestion', null=True, on_delete=models.deletion.SET_NULL
    )
    whiteboard = models.ForeignKey(
        'WhiteboardMessage', null=True, on_delete=models.deletion.SET_NULL
    )
    alert = models.ForeignKey('Alert', null=True, on_delete=models.deletion.SET_NULL)
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL, null=True, on_delete=models.deletion.CASCADE
    )
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        related_name='author_set',
        on_delete=models.deletion.CASCADE,
    )
    timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
    action = models.IntegerField(choices=ACTION_CHOICES, default=ACTION_CHANGE)
    target = models.TextField(default='', blank=True)
    old = models.TextField(default='', blank=True)
    details = JSONField()

    objects = ChangeManager.from_queryset(ChangeQuerySet)()

    class Meta(object):
        app_label = 'trans'

    def __init__(self, *args, **kwargs):
        self.notify_state = {}
        super(Change, self).__init__(*args, **kwargs)

    def __str__(self):
        return _('%(action)s at %(time)s on %(translation)s by %(user)s') % {
            'action': self.get_action_display(),
            'time': self.timestamp,
            'translation': self.translation,
            'user': self.get_user_display(False),
        }

    def is_merge_failure(self):
        return self.action in self.ACTIONS_MERGE_FAILURE

    def get_absolute_url(self):
        """Return link either to unit or translation."""
        if self.unit is not None:
            return self.unit.get_absolute_url()
        if self.translation is not None:
            return self.translation.get_absolute_url()
        if self.component is not None:
            return self.component.get_absolute_url()
        if self.dictionary is not None:
            return self.dictionary.get_parent_url()
        if self.project is not None:
            return self.project.get_absolute_url()
        return None

    def can_revert(self):
        return (
            self.unit is not None
            and self.old
            and self.action in self.ACTIONS_REVERTABLE
        )

    def show_source(self):
        """Whether to show content as source change."""
        return self.action == self.ACTION_SOURCE_CHANGE

    def show_content(self):
        """Whether to show content as translation."""
        return (
            self.action in self.ACTIONS_SHOW_CONTENT
            or self.action in self.ACTIONS_REVERTABLE
        )

    def get_details_display(self):
        from weblate.utils.markdown import render_markdown

        if not self.details:
            return ''
        user_actions = {
            self.ACTION_ADD_USER,
            self.ACTION_INVITE_USER,
            self.ACTION_REMOVE_USER,
        }

        if self.action == self.ACTION_ACCESS_EDIT:
            for number, name in Project.ACCESS_CHOICES:
                if number == self.details['access_control']:
                    return name
            return 'Unknonwn {}'.format(self.details['access_control'])
        elif self.action in user_actions:
            if 'group' in self.details:
                return '{username} ({group})'.format(**self.details)
            return self.details['username']
        elif self.action in (
            self.ACTION_ADDED_LANGUAGE,
            self.ACTION_REQUESTED_LANGUAGE,
        ):  # noqa: E501
            try:
                return Language.objects.get(code=self.details['language'])
            except Language.DoesNotExist:
                return self.details['language']
        elif self.action == self.ACTION_ALERT:
            try:
                return ALERTS[self.details['alert']].verbose
            except KeyError:
                return self.details['alert']
        elif self.action == self.ACTION_PARSE_ERROR:
            return '{filename}: {error_message}'.format(**self.details)
        elif self.action == self.ACTION_HOOK:
            return '{service_long_name}: {repo_url}, {branch}'.format(**self.details)
        elif self.action == self.ACTION_COMMENT and 'comment' in self.details:
            return render_markdown(self.details['comment'])

        return ''

    def save(self, *args, **kwargs):
        from weblate.accounts.tasks import notify_change

        if self.unit:
            self.translation = self.unit.translation
        if self.translation:
            self.component = self.translation.component
        if self.component:
            self.project = self.component.project
        if self.dictionary:
            self.project = self.dictionary.project
        super(Change, self).save(*args, **kwargs)
        transaction.on_commit(lambda: notify_change.delay(self.pk))
