# Copyright (C) 2016-2024 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 colander
import deform.widget

from rhodecode.model.validation_schema.utils import username_converter
from rhodecode.translation import _
from rhodecode.model.validation_schema import validators, preparers, types


def get_group_and_repo(repo_name):
    from rhodecode.model.repo_group import RepoGroupModel

    return RepoGroupModel()._get_group_name_and_parent(repo_name, get_object=True)


def get_repo_group(repo_group_id):
    from rhodecode.model.repo_group import RepoGroup

    return RepoGroup.get(repo_group_id), RepoGroup.CHOICES_SEPARATOR


@colander.deferred
def deferred_can_write_to_group_validator(node, kw):
    old_values = kw.get("old_values") or {}
    request_user = kw.get("user")

    def can_write_group_validator(node, value):
        from rhodecode.lib.auth import HasPermissionAny, HasRepoGroupPermissionAny
        from rhodecode.model.repo_group import RepoGroupModel

        messages = {
            "invalid_parent_repo_group": _("Parent repository group `{}` does not exist"),
            # permissions denied we expose as not existing, to prevent
            # resource discovery
            "permission_denied_parent_group": _(
                "You do not have the permissions to store repository groups inside repository group `{}`"
            ),
            "permission_denied_root": _(
                "You do not have the permission to store repository groups in the root location."
            ),
        }

        value = value["repo_group_name"]
        parent_group_name = value

        is_root_location = value is types.RootLocation

        # NOT initialized validators, we must call them
        can_create_repo_groups_at_root = HasPermissionAny("hg.admin", "hg.repogroup.create.true")

        if is_root_location:
            if can_create_repo_groups_at_root(user=request_user):
                # we can create repo group inside tool-level. No more checks
                # are required
                return
            else:
                raise colander.Invalid(node, messages["permission_denied_root"])

        # check if the parent repo group actually exists
        parent_group = None
        if parent_group_name:
            parent_group = RepoGroupModel().get_by_group_name(parent_group_name)
            if value and not parent_group:
                raise colander.Invalid(node, messages["invalid_parent_repo_group"].format(parent_group_name))

        # check if we have permissions to create new groups under
        # parent repo group
        # create repositories with write permission on group is set to true
        create_on_write = HasPermissionAny("hg.create.write_on_repogroup.true")(user=request_user)

        group_admin = HasRepoGroupPermissionAny("group.admin")(
            parent_group_name, "can write into group validator", user=request_user
        )
        group_write = HasRepoGroupPermissionAny("group.write")(
            parent_group_name, "can write into group validator", user=request_user
        )

        # creation by write access is currently disabled. Needs thinking if
        # we want to allow this...
        forbidden = not (group_admin or (group_write and create_on_write and 0))

        old_name = old_values.get("group_name")
        if old_name and old_name == old_values.get("submitted_repo_group_name"):
            # we're editing a repository group, we didn't change the name
            # we skip the check for write into parent group now
            # this allows changing settings for this repo group
            return

        if parent_group and forbidden:
            msg = messages["permission_denied_parent_group"].format(parent_group_name)
            raise colander.Invalid(node, msg)

    return can_write_group_validator


@colander.deferred
def deferred_repo_group_owner_validator(node, kw):
    def repo_owner_validator(node, value):
        from rhodecode.model.db import User

        value = username_converter(value)
        existing = User.get_by_username(value)
        if not existing:
            msg = _("Repo group owner with id `{}` does not exists").format(value)
            raise colander.Invalid(node, msg)

    return repo_owner_validator


@colander.deferred
def deferred_unique_name_validator(node, kw):
    request_user = kw.get("user")
    old_values = kw.get("old_values") or {}

    def unique_name_validator(node, value):
        from rhodecode.model.db import Repository, RepoGroup

        name_changed = value != old_values.get("group_name")

        existing = Repository.get_by_repo_name(value)
        if name_changed and existing:
            msg = _("Repository with name `{}` already exists").format(value)
            raise colander.Invalid(node, msg)

        existing_group = RepoGroup.get_by_group_name(value)
        if name_changed and existing_group:
            msg = _("Repository group with name `{}` already exists").format(value)
            raise colander.Invalid(node, msg)

    return unique_name_validator


@colander.deferred
def deferred_repo_group_name_validator(node, kw):
    return colander.All(validators.valid_name_validator, validators.forbidden_repo_group_name_validator)


@colander.deferred
def deferred_repo_group_validator(node, kw):
    options = kw.get("repo_group_repo_group_options")
    return colander.OneOf([x for x in options])


@colander.deferred
def deferred_repo_group_widget(node, kw):
    items = kw.get("repo_group_repo_group_items")
    return deform.widget.Select2Widget(values=items)


class GroupType(colander.Mapping):
    def _validate(self, node, value):
        try:
            return dict(repo_group_name=value)
        except Exception as e:
            raise colander.Invalid(node, f'"${value}" is not a mapping type: ${e}')

    def deserialize(self, node, cstruct):
        if cstruct is colander.null:
            return cstruct

        appstruct = super().deserialize(node, cstruct)
        validated_name = appstruct["repo_group_name"]

        # inject a group based on deserialized data
        (repo_group_name_without_group, parent_group_name, parent_group) = get_group_and_repo(validated_name)

        appstruct["repo_group_name_with_group"] = validated_name
        appstruct["repo_group_name_without_group"] = repo_group_name_without_group
        appstruct["repo_group_name"] = parent_group_name or types.RootLocation
        if parent_group:
            appstruct["repo_group_id"] = parent_group.group_id

        return appstruct


class GroupSchema(colander.SchemaNode):
    schema_type = GroupType
    validator = deferred_can_write_to_group_validator
    missing = colander.null


class RepoGroup(GroupSchema):
    repo_group_name = colander.SchemaNode(types.GroupNameType())
    repo_group_id = colander.SchemaNode(colander.String(), missing=None)
    repo_group_name_without_group = colander.SchemaNode(colander.String(), missing=None)


class RepoGroupAccessSchema(colander.MappingSchema):
    repo_group = RepoGroup()


class RepoGroupNameUniqueSchema(colander.MappingSchema):
    unique_repo_group_name = colander.SchemaNode(colander.String(), validator=deferred_unique_name_validator)


class RepoGroupSchema(colander.Schema):
    repo_group_name = colander.SchemaNode(types.GroupNameType(), validator=deferred_repo_group_name_validator)

    repo_group_owner = colander.SchemaNode(colander.String(), validator=deferred_repo_group_owner_validator)

    repo_group_description = colander.SchemaNode(colander.String(), missing="", widget=deform.widget.TextAreaWidget())

    repo_group_copy_permissions = colander.SchemaNode(
        types.StringBooleanType(), missing=False, widget=deform.widget.CheckboxWidget()
    )

    repo_group_enable_locking = colander.SchemaNode(
        types.StringBooleanType(), missing=False, widget=deform.widget.CheckboxWidget()
    )

    def deserialize(self, cstruct):
        """
        Custom deserialize that allows to chain validation, and verify
        permissions, and as last step uniqueness
        """

        appstruct = super().deserialize(cstruct)
        validated_name = appstruct["repo_group_name"]

        # second pass to validate permissions to repo_group
        if "old_values" in self.bindings:
            # save current repo name for name change checks
            self.bindings["old_values"]["submitted_repo_group_name"] = validated_name
        second = RepoGroupAccessSchema().bind(**self.bindings)
        appstruct_second = second.deserialize({"repo_group": validated_name})
        # save result
        appstruct["repo_group"] = appstruct_second["repo_group"]

        # thirds to validate uniqueness
        third = RepoGroupNameUniqueSchema().bind(**self.bindings)
        third.deserialize({"unique_repo_group_name": validated_name})

        return appstruct


class RepoGroupSettingsSchema(RepoGroupSchema):
    repo_group = colander.SchemaNode(
        colander.Integer(), validator=deferred_repo_group_validator, widget=deferred_repo_group_widget, missing=""
    )

    def deserialize(self, cstruct):
        """
        Custom deserialize that allows to chain validation, and verify
        permissions, and as last step uniqueness
        """

        # first pass, to validate given data
        appstruct = super(RepoGroupSchema, self).deserialize(cstruct)
        validated_name = appstruct["repo_group_name"]

        # because of repoSchema adds repo-group as an ID, we inject it as
        # full name here because validators require it, it's unwrapped later
        # so it's safe to use and final name is going to be without group anyway

        group, separator = get_repo_group(appstruct["repo_group"])
        if group:
            validated_name = separator.join([group.group_name, validated_name])

        # second pass to validate permissions to repo_group
        if "old_values" in self.bindings:
            # save current repo name for name change checks
            self.bindings["old_values"]["submitted_repo_group_name"] = validated_name
        second = RepoGroupAccessSchema().bind(**self.bindings)
        appstruct_second = second.deserialize({"repo_group": validated_name})
        # save result
        appstruct["repo_group"] = appstruct_second["repo_group"]

        # thirds to validate uniqueness
        third = RepoGroupNameUniqueSchema().bind(**self.bindings)
        third.deserialize({"unique_repo_group_name": validated_name})

        return appstruct
