# Copyright 2024 The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.

"""
debusine-admin command to manage scopes.

Note: to make commands easier to be invoked from Ansible, we take care to make
them idempotent.
"""

from collections.abc import Callable
from typing import NoReturn, cast

from django.core.exceptions import ValidationError
from django.core.management import CommandError
from django.db import transaction

from debusine.db.models import Group, Scope
from debusine.db.models.scopes import ScopeRole
from debusine.django.management.debusine_base_command import DebusineBaseCommand


class Command(DebusineBaseCommand):
    """Command to manage scopes."""

    help = "Manage scopes"

    def add_arguments(self, parser):
        """Add CLI arguments."""
        subparsers = parser.add_subparsers(dest="action", required=True)

        create = subparsers.add_parser("create", help="Ensure a scope exists")
        create.add_argument("name", help="Name for the new scope")
        create.add_argument(
            "--with-owners-group",
            type=str,
            nargs="?",
            metavar="name",
            const="Owners",
            help="Ensure the scope has the named owners group"
            " (name defaults to 'Owners')",
        )

        rename = subparsers.add_parser("rename", help="Rename a scope")
        rename.add_argument("scope", help="Scope to rename")
        rename.add_argument("name", help="New name for the scope")

    def get_scope(self, name: str) -> Scope:
        """Lookup a scope by name."""
        try:
            return Scope.objects.get(name=name)
        except Scope.DoesNotExist:
            raise CommandError(f"Scope {name!r} not found", returncode=3)

    def handle(self, *args, **options) -> NoReturn:
        """Dispatch the requested action."""
        func = cast(
            Callable[..., NoReturn],
            getattr(self, f"handle_{options['action']}", None),
        )
        if func is None:
            raise CommandError(
                f"Action {options['action']!r} not found", returncode=3
            )

        func(*args, **options)

    def handle_create(
        self, *, name: str, with_owners_group: str | None = None, **options
    ) -> NoReturn:
        """
        Create a scope, initialized with an Owners group.

        This is idempotent, and it makes sure the named scope exists, it has an
        "Owners" group, and that group has the ADMIN role on the scope.
        """
        with transaction.atomic():
            scope, _ = Scope.objects.get_or_create(name=name)
            if with_owners_group:
                # Make sure the scope has an Owners group
                admin_group, _ = Group.objects.get_or_create(
                    scope=scope, name=with_owners_group
                )
                scope.assign_role(ScopeRole.Roles.OWNER, admin_group)

        raise SystemExit(0)

    def handle_rename(self, *, scope: str, name: str, **options) -> NoReturn:
        """Rename a scope."""
        s = self.get_scope(scope)

        if name == s.name:
            raise SystemExit(0)

        s.name = name
        try:
            s.full_clean()
        except ValidationError as exc:
            self.stderr.write("Renamed scope would be invalid:")
            for field, errors in exc.message_dict.items():
                for error in errors:
                    self.stderr.write(f"* {field}: {error}")
            raise SystemExit(3)

        s.save()

        raise SystemExit(0)
