import yaml
from django.contrib.auth.models import Permission
from django.db import transaction

from topdownrbac.models import Role


class RoleSyncService:
    """
    Loads roles + permissions from a YAML configuration file and syncs them with the database.
    """

    def __init__(self, yaml_path: str):
        self.yaml_path = yaml_path

    def load_yaml(self):
        with open(self.yaml_path) as f:
            return yaml.safe_load(f)

    @transaction.atomic
    def sync(self):
        data = self.load_yaml()

        if "roles" not in data:
            raise ValueError("YAML must have a top-level `roles:` key")

        roles_data = data["roles"]

        yaml_role_names = set(roles_data.keys())
        db_provisioned_roles = set(
            Role.objects.filter(externally_provisioned=True, is_active=True).values_list("name", flat=True)
        )

        deactivated_roles = []

        # deactivate provisioned roles removed from the yaml
        for role_name in db_provisioned_roles - yaml_role_names:
            role = Role.objects.get(name=role_name)
            role.deactivate()
            deactivated_roles.append(role_name)

        role_updates = []

        # create or update roles
        for role_name, role_def in roles_data.items():
            update = self.sync_single_role(role_name, role_def)
            if update[3] or update[4] or not (len(update[1]) == 0 and len(update[2]) == 0):
                role_updates.append(update)

        return deactivated_roles, role_updates

    def sync_single_role(self, role_name: str, role_def: dict):
        role, created = Role.objects.get_or_create(
            name=role_name,
            defaults={"externally_provisioned": True, "editable": False},
        )

        # reactivate if previously deactivated
        reactivated = False
        if not created and not role.is_active:
            role.reactivate()
            reactivated = True

        role.set_editable()

        expected_permissions = role_def.get("permissions", [])

        # get permission objects that actually exist with the provided codenames
        perm_objs = list(Permission.objects.filter(codename__in=expected_permissions))
        # if there are any missing point them out and raise error
        found_codenames = {p.codename for p in perm_objs}
        missing = set(expected_permissions) - found_codenames

        if missing:
            raise ValueError(f"Permissions not found in database: {', '.join(missing)}")

        new_perms = role.add_many_permissions(perm_objs)

        removed_perms = []

        # delete any permission that was removed from yaml
        current = set(role.permissions.values_list("codename", flat=True))
        for perm_codename in current - set(expected_permissions):
            perm = Permission.objects.get(codename=perm_codename)
            role.remove_permission(perm)
            removed_perms.append(perm_codename)

        role.set_immutable()

        return role.name, new_perms, removed_perms, created, reactivated
