# Copyright (C) 2011-2023 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# 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 Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/

import logging
import datetime
import formencode
import formencode.htmlfill

from pyramid.httpexceptions import HTTPFound

from pyramid.renderers import render
from pyramid.response import Response

from rhodecode import events
from rhodecode.apps._base import RepoAppView, DataGridAppView
from rhodecode.lib.auth import (
    LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
    HasRepoPermissionAny, HasPermissionAnyDecorator, CSRFRequired)
import rhodecode.lib.helpers as h
from rhodecode.lib.str_utils import safe_str
from rhodecode.lib.celerylib.utils import get_task_id
from rhodecode.model.db import coalesce, or_, Repository, RepoGroup
from rhodecode.model.permission import PermissionModel
from rhodecode.model.repo import RepoModel
from rhodecode.model.forms import RepoForkForm
from rhodecode.model.scm import ScmModel, RepoGroupList

log = logging.getLogger(__name__)


class RepoForksView(RepoAppView, DataGridAppView):

    def load_default_context(self):
        c = self._get_local_tmpl_context(include_app_defaults=True)
        c.rhodecode_repo = self.rhodecode_vcs_repo

        acl_groups = RepoGroupList(
            RepoGroup.query().all(),
            perm_set=['group.write', 'group.admin'])
        c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
        c.repo_groups_choices = list(map(lambda k: safe_str(k[0]), c.repo_groups))

        c.personal_repo_group = c.rhodecode_user.personal_repo_group

        return c

    @LoginRequired()
    @HasRepoPermissionAnyDecorator(
        'repository.read', 'repository.write', 'repository.admin')
    def repo_forks_show_all(self):
        c = self.load_default_context()
        return self._get_template_context(c)

    @LoginRequired()
    @HasRepoPermissionAnyDecorator(
        'repository.read', 'repository.write', 'repository.admin')
    def repo_forks_data(self):
        _ = self.request.translate
        self.load_default_context()
        column_map = {
            'fork_name': 'repo_name',
            'fork_date': 'created_on',
            'last_activity': 'updated_on'
        }
        draw, start, limit = self._extract_chunk(self.request)
        search_q, order_by, order_dir = self._extract_ordering(
            self.request, column_map=column_map)

        acl_check = HasRepoPermissionAny(
            'repository.read', 'repository.write', 'repository.admin')
        repo_id = self.db_repo.repo_id
        allowed_ids = [-1]
        for f in Repository.query().filter(Repository.fork_id == repo_id):
            if acl_check(f.repo_name, 'get forks check'):
                allowed_ids.append(f.repo_id)

        forks_data_total_count = Repository.query()\
            .filter(Repository.fork_id == repo_id)\
            .filter(Repository.repo_id.in_(allowed_ids))\
            .count()

        # json generate
        base_q = Repository.query()\
            .filter(Repository.fork_id == repo_id)\
            .filter(Repository.repo_id.in_(allowed_ids))\

        if search_q:
            like_expression = f'%{safe_str(search_q)}%'
            base_q = base_q.filter(or_(
                Repository.repo_name.ilike(like_expression),
                Repository.description.ilike(like_expression),
            ))

        forks_data_total_filtered_count = base_q.count()

        sort_col = getattr(Repository, order_by, None)
        if sort_col:
            if order_dir == 'asc':
                # handle null values properly to order by NULL last
                if order_by in ['last_activity']:
                    sort_col = coalesce(sort_col, datetime.date.max)
                sort_col = sort_col.asc()
            else:
                # handle null values properly to order by NULL last
                if order_by in ['last_activity']:
                    sort_col = coalesce(sort_col, datetime.date.min)
                sort_col = sort_col.desc()

        base_q = base_q.order_by(sort_col)
        base_q = base_q.offset(start).limit(limit)

        fork_list = base_q.all()

        def fork_actions(fork):
            url_link = h.route_path(
                'repo_compare',
                repo_name=fork.repo_name,
                source_ref_type=self.db_repo.landing_ref_type,
                source_ref=self.db_repo.landing_ref_name,
                target_ref_type=self.db_repo.landing_ref_type,
                target_ref=self.db_repo.landing_ref_name,
                _query=dict(merge=1, target_repo=f.repo_name))
            return h.link_to(_('Compare fork'), url_link, class_='btn-link')

        def fork_name(fork):
            return h.link_to(fork.repo_name,
                      h.route_path('repo_summary', repo_name=fork.repo_name))

        forks_data = []
        for fork in fork_list:
            forks_data.append({
                "username": h.gravatar_with_user(self.request, fork.user.username),
                "fork_name": fork_name(fork),
                "description": fork.description_safe,
                "fork_date": h.age_component(fork.created_on, time_is_local=True),
                "last_activity": h.format_date(fork.updated_on),
                "action": fork_actions(fork),
            })

        data = ({
            'draw': draw,
            'data': forks_data,
            'recordsTotal': forks_data_total_count,
            'recordsFiltered': forks_data_total_filtered_count,
        })

        return data

    @LoginRequired()
    @NotAnonymous()
    @HasPermissionAnyDecorator('hg.admin', PermissionModel.FORKING_ENABLED)
    @HasRepoPermissionAnyDecorator(
        'repository.read', 'repository.write', 'repository.admin')
    def repo_fork_new(self):
        c = self.load_default_context()

        defaults = RepoModel()._get_defaults(self.db_repo_name)
        # alter the description to indicate a fork
        defaults['description'] = (
            'fork of repository: {} \n{}'.format(
                defaults['repo_name'], defaults['description']))
        # add suffix to fork
        defaults['repo_name'] = '%s-fork' % defaults['repo_name']

        data = render('rhodecode:templates/forks/fork.mako',
                      self._get_template_context(c), self.request)
        html = formencode.htmlfill.render(
            data,
            defaults=defaults,
            encoding="UTF-8",
            force_defaults=False
        )
        return Response(html)

    @LoginRequired()
    @NotAnonymous()
    @HasPermissionAnyDecorator('hg.admin', PermissionModel.FORKING_ENABLED)
    @HasRepoPermissionAnyDecorator(
        'repository.read', 'repository.write', 'repository.admin')
    @CSRFRequired()
    def repo_fork_create(self):
        _ = self.request.translate
        c = self.load_default_context()

        _form = RepoForkForm(self.request.translate,
                             old_data={'repo_type': self.db_repo.repo_type},
                             repo_groups=c.repo_groups_choices)()
        post_data = dict(self.request.POST)

        # forbid injecting other repo by forging a request
        post_data['fork_parent_id'] = self.db_repo.repo_id
        post_data['landing_rev'] = self.db_repo._landing_revision

        form_result = {}
        task_id = None
        try:
            form_result = _form.to_python(post_data)
            copy_permissions = form_result.get('copy_permissions')
            # create fork is done sometimes async on celery, db transaction
            # management is handled there.
            task = RepoModel().create_fork(
                form_result, c.rhodecode_user.user_id)

            task_id = get_task_id(task)
        except formencode.Invalid as errors:
            c.rhodecode_db_repo = self.db_repo

            data = render('rhodecode:templates/forks/fork.mako',
                          self._get_template_context(c), self.request)
            html = formencode.htmlfill.render(
                data,
                defaults=errors.value,
                errors=errors.error_dict or {},
                prefix_error=False,
                encoding="UTF-8",
                force_defaults=False
            )
            return Response(html)
        except Exception:
            log.exception(
                'Exception while trying to fork the repository %s', self.db_repo_name)
            msg = _('An error occurred during repository forking %s') % (self.db_repo_name, )
            h.flash(msg, category='error')
            raise HTTPFound(h.route_path('home'))

        repo_name = form_result.get('repo_name_full', self.db_repo_name)

        affected_user_ids = [self._rhodecode_user.user_id]
        if copy_permissions:
            # permission flush is done in repo creating
            pass

        PermissionModel().trigger_permission_flush(affected_user_ids)

        raise HTTPFound(
            h.route_path('repo_creating', repo_name=repo_name,
                         _query=dict(task_id=task_id)))
